diff --git a/docs/modules/Custom.md b/docs/modules/Custom.md
index 04c0f07..9b1b28d 100644
--- a/docs/modules/Custom.md
+++ b/docs/modules/Custom.md
@@ -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.

@@ -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:
@@ -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": "",
"type": "button"
},
{
"class": "power-btn",
- "exec": "!reboot",
+ "on_click": "!reboot",
"label": "",
"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:
TOML
```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 = ''''''
type = 'button'
[[end.popup.widgets.widgets]]
class = 'power-btn'
-exec = '!reboot'
+on_click = '!reboot'
label = ''''''
type = 'button'
+
+[[end.popup.widgets]]
+label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
+name = 'uptime'
+type = 'label'
```
@@ -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:
type: button
- class: power-btn
- exec: '!reboot'
+ on_click: '!reboot'
label:
type: button
+ - label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
+ name: uptime
+ type: label
+ type: custom
```
@@ -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 = "" exec = "!shutdown now" }
- { type = "button" class="power-btn" label = "" exec = "!reboot" }
+ { type = "button" class="power-btn" label = "" on_click = "!shutdown now" }
+ { type = "button" class="power-btn" label = "" on_click = "!reboot" }
]
}
+ { type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
]
} ]
}
diff --git a/src/config.rs b/src/config.rs
index c6c7ddf..8d251b7 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -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,
diff --git a/src/main.rs b/src/main.rs
index 9aeb9bd..d9ffbaf 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,6 +8,7 @@ mod modules;
mod popup;
mod script;
mod style;
+mod widgets;
use crate::bar::create_bar;
use crate::config::{Config, MonitorConfig};
diff --git a/src/modules/custom.rs b/src/modules/custom.rs
index 159c22e..7ef990b 100644
--- a/src/modules/custom.rs
+++ b/src/modules/custom.rs
@@ -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: >k::Box, tx: Sender, 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
diff --git a/src/widgets/dynamic_label.rs b/src/widgets/dynamic_label.rs
new file mode 100644
index 0000000..30cc2c9
--- /dev/null
+++ b/src/widgets/dynamic_label.rs
@@ -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::>();
+ 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::();
+
+ 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::();
+
+ 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::();
+
+ 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-}}");
+ }
+}
diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs
new file mode 100644
index 0000000..a1fdcd8
--- /dev/null
+++ b/src/widgets/mod.rs
@@ -0,0 +1,3 @@
+mod dynamic_label;
+
+pub use dynamic_label::DynamicLabel;