feat(custom): ability to embed scripts in labels for dynamic content

Fully resolves #34.
This commit is contained in:
Jake Stanger 2022-11-28 22:27:31 +00:00
parent e274ba39cd
commit 5d153a02fc
No known key found for this signature in database
GPG Key ID: C51FC8F9CB0BEA61
6 changed files with 196 additions and 33 deletions

View File

@ -1,5 +1,5 @@
Allows you to compose custom modules consisting of multiple widgets, including popups.
Buttons can interact with the bar or execute commands on click.
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
![Custom module with a button on the bar, and the popup open. The popup contains a header, shutdown button and restart button.](https://user-images.githubusercontent.com/5057870/196058785-042ef171-7e77-4d5c-921a-eca03c6424bd.png)
@ -24,10 +24,23 @@ It is well worth looking at the examples.
| `name` | `string` | `null` | Widget name. |
| `class` | `string` | `null` | Widget class name. |
| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. |
| `exec` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). |
| `orientation` | `horizontal` or `vertical` | `horizontal` | [`box`] Whether child widgets should be horizontally or vertically added. |
| `widgets` | `Widget[]` | `[]` | [`box`] List of widgets to add to this box. |
### Labels
Labels can interpolate text from scripts to dynamically show content.
This can be done by including scripts in `{{double braces}}` using the shorthand script syntax.
For example, the following label would output your system uptime, updated every 30 seconds.
```
Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}
```
Both polling and watching mode are supported. For more information on script syntax, see [here](script).
### Commands
Buttons can execute commands that interact with the bar,
@ -35,7 +48,7 @@ as well as any arbitrary shell command.
To execute shell commands, prefix them with an `!`.
For example, if you want to run `~/.local/bin/my-script.sh` on click,
you'd set `exec` to `!~/.local/bin/my-script.sh`.
you'd set `on_click` to `!~/.local/bin/my-script.sh`.
The following bar commands are supported:
@ -44,7 +57,7 @@ The following bar commands are supported:
- `popup:close`
XML is arguably better-suited and easier to read for this sort of markup,
but currently not supported.
but currently is not supported.
Nonetheless, it may be worth comparing the examples to the below equivalent
to help get your head around what's going on:
@ -53,15 +66,16 @@ to help get your head around what's going on:
<?xml version="1.0" encoding="utf-8" ?>
<custom class="power-menu">
<bar>
<button name="power-btn" label="" exec="popup:toggle"/>
<button name="power-btn" label="" on_click="popup:toggle"/>
</bar>
<popup>
<box orientation="vertical">
<label name="header" label="Power menu" />
<box>
<button class="power-btn" label="" exec="!shutdown now" />
<button class="power-btn" label="" exec="!reboot" />
<button class="power-btn" label="" on_click="!shutdown now" />
<button class="power-btn" label="" on_click="!reboot" />
</box>
<label name="uptime" label="Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" />
</box>
</popup>
</custom>
@ -74,10 +88,12 @@ to help get your head around what's going on:
{
"end": [
{
"type": "custom",
"type": "clock"
},
{
"bar": [
{
"exec": "popup:toggle",
"on_click": "popup:toggle",
"label": "",
"name": "power-btn",
"type": "button"
@ -99,21 +115,27 @@ to help get your head around what's going on:
"widgets": [
{
"class": "power-btn",
"exec": "!shutdown now",
"on_click": "!shutdown now",
"label": "<span font-size='40pt'></span>",
"type": "button"
},
{
"class": "power-btn",
"exec": "!reboot",
"on_click": "!reboot",
"label": "<span font-size='40pt'></span>",
"type": "button"
}
]
},
{
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
"name": "uptime",
"type": "label"
}
]
}
]
],
"type": "custom"
}
]
}
@ -125,12 +147,15 @@ to help get your head around what's going on:
<summary>TOML</summary>
```toml
[[end]]
type = 'clock'
[[end]]
class = 'power-menu'
type = 'custom'
[[end.bar]]
exec = 'popup:toggle'
on_click = 'popup:toggle'
label = ''
name = 'power-btn'
type = 'button'
@ -149,15 +174,20 @@ type = 'box'
[[end.popup.widgets.widgets]]
class = 'power-btn'
exec = '!shutdown now'
on_click = '!shutdown now'
label = '''<span font-size='40pt'></span>'''
type = 'button'
[[end.popup.widgets.widgets]]
class = 'power-btn'
exec = '!reboot'
on_click = '!reboot'
label = '''<span font-size='40pt'></span>'''
type = 'button'
[[end.popup.widgets]]
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
name = 'uptime'
type = 'label'
```
</details>
@ -167,9 +197,9 @@ type = 'button'
```yaml
end:
- type: custom
bar:
- exec: popup:toggle
- type: clock
- bar:
- on_click: popup:toggle
label: 
name: power-btn
type: button
@ -184,13 +214,17 @@ end:
- type: box
widgets:
- class: power-btn
exec: '!shutdown now'
on_click: '!shutdown now'
label: <span font-size='40pt'></span>
type: button
- class: power-btn
exec: '!reboot'
on_click: '!reboot'
label: <span font-size='40pt'></span>
type: button
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
name: uptime
type: label
type: custom
```
</details>
@ -204,7 +238,7 @@ let {
type = "custom"
class = "power-menu"
bar = [ { type = "button" name="power-btn" label = "" exec = "popup:toggle" } ]
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
popup = [ {
type = "box"
@ -214,10 +248,11 @@ let {
{
type = "box"
widgets = [
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" exec = "!shutdown now" }
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" exec = "!reboot" }
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
]
}
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
} ]
}

View File

@ -27,7 +27,7 @@ pub struct CommonConfig {
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
Clock(ClockModule),
Mpd(MpdModule),
@ -48,7 +48,7 @@ pub enum MonitorConfig {
}
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[serde(rename_all = "snake_case")]
pub enum BarPosition {
Top,
Bottom,

View File

@ -8,6 +8,7 @@ mod modules;
mod popup;
mod script;
mod style;
mod widgets;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};

View File

@ -4,6 +4,7 @@ use crate::config::CommonConfig;
use color_eyre::{Report, Result};
use crate::script::Script;
use gtk::prelude::*;
use crate::widgets::DynamicLabel;
use gtk::{Button, Label, Orientation};
use serde::Deserialize;
use tokio::spawn;
@ -48,7 +49,7 @@ pub struct Widget {
/// Supported GTK widget types
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
#[serde(rename_all = "snake_case")]
pub enum WidgetType {
Box,
Label,
@ -60,7 +61,7 @@ impl Widget {
fn add_to(self, parent: &gtk::Box, tx: Sender<ExecEvent>, bar_orientation: Orientation) {
match self.widget_type {
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation)),
WidgetType::Label => parent.add(&self.into_label()),
WidgetType::Label => parent.add(&self.into_label().label),
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
}
}
@ -94,7 +95,7 @@ impl Widget {
}
/// Creates a `gtk::Label` from this widget
fn into_label(self) -> Label {
fn into_label(self) -> DynamicLabel {
let mut builder = Label::builder().use_markup(true);
if let Some(name) = self.name {
@ -103,15 +104,13 @@ impl Widget {
let label = builder.build();
if let Some(text) = self.label {
label.set_markup(&text);
}
if let Some(class) = self.class {
label.style_context().add_class(&class);
}
label
let text = self.label.map_or_else(String::new, |text| text);
DynamicLabel::new(label, &text)
}
/// Creates a `gtk::Button` from this widget

View File

@ -0,0 +1,125 @@
use crate::script::{OutputStream, Script};
use gtk::prelude::*;
use indexmap::IndexMap;
use std::sync::{Arc, Mutex};
use tokio::spawn;
#[derive(Debug)]
enum DynamicLabelSegment {
Static(String),
Dynamic(Script),
}
pub struct DynamicLabel {
pub label: gtk::Label,
}
impl DynamicLabel {
pub fn new(label: gtk::Label, input: &str) -> Self {
let mut segments = vec![];
let mut chars = input.chars().collect::<Vec<_>>();
while !chars.is_empty() {
let char = &chars[..=1];
let (token, skip) = if let ['{', '{'] = char {
const SKIP_BRACKETS: usize = 4;
let str = chars
.iter()
.skip(2)
.enumerate()
.take_while(|(i, &c)| c != '}' && chars[i + 1] != '}')
.map(|(_, c)| c)
.collect::<String>();
let len = str.len();
(
DynamicLabelSegment::Dynamic(Script::from(str.as_str())),
len + SKIP_BRACKETS,
)
} else {
let str = chars
.iter()
.enumerate()
.take_while(|(i, &c)| !(c == '{' && chars[i + 1] == '{'))
.map(|(_, c)| c)
.collect::<String>();
let len = str.len();
(DynamicLabelSegment::Static(str), len)
};
assert_ne!(skip, 0);
segments.push(token);
chars.drain(..skip);
}
let label_parts = Arc::new(Mutex::new(IndexMap::new()));
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
for (i, segment) in segments.into_iter().enumerate() {
match segment {
DynamicLabelSegment::Static(str) => {
label_parts
.lock()
.expect("Failed to get lock on label parts")
.insert(i, str);
}
DynamicLabelSegment::Dynamic(script) => {
let tx = tx.clone();
let label_parts = label_parts.clone();
spawn(async move {
script
.run(|(out, _)| {
if let OutputStream::Stdout(out) = out {
label_parts
.lock()
.expect("Failed to get lock on label parts")
.insert(i, out);
tx.send(()).expect("Failed to send update");
}
})
.await;
});
}
}
}
tx.send(()).expect("Failed to send update");
{
let label = label.clone();
rx.attach(None, move |_| {
let new_label = label_parts
.lock()
.expect("Failed to get lock on label parts")
.iter()
.map(|(_, part)| part.as_str())
.collect::<String>();
label.set_label(new_label.as_str());
Continue(true)
});
}
Self { label }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test() {
gtk::init().unwrap();
let label = gtk::Label::new(None);
DynamicLabel::new(label, "Uptime: {{1000:uptime -p | cut -d ' ' -f2-}}");
}
}

3
src/widgets/mod.rs Normal file
View File

@ -0,0 +1,3 @@
mod dynamic_label;
pub use dynamic_label::DynamicLabel;