From 2ab06f044ec300628d6648852d395889b6752b76 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Fri, 7 Apr 2023 20:22:31 +0100 Subject: [PATCH 1/7] refactor(custom): split into enum with separate file per widget --- src/modules/custom.rs | 309 ----------------------------------- src/modules/custom/box.rs | 43 +++++ src/modules/custom/button.rs | 56 +++++++ src/modules/custom/image.rs | 43 +++++ src/modules/custom/label.rs | 42 +++++ src/modules/custom/mod.rs | 187 +++++++++++++++++++++ 6 files changed, 371 insertions(+), 309 deletions(-) delete mode 100644 src/modules/custom.rs create mode 100644 src/modules/custom/box.rs create mode 100644 src/modules/custom/button.rs create mode 100644 src/modules/custom/image.rs create mode 100644 src/modules/custom/label.rs create mode 100644 src/modules/custom/mod.rs diff --git a/src/modules/custom.rs b/src/modules/custom.rs deleted file mode 100644 index 13af4f0..0000000 --- a/src/modules/custom.rs +++ /dev/null @@ -1,309 +0,0 @@ -use crate::config::CommonConfig; -use crate::dynamic_string::DynamicString; -use crate::image::ImageProvider; -use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; -use crate::popup::{ButtonGeometry, Popup}; -use crate::script::Script; -use crate::{send_async, try_send}; -use color_eyre::{Report, Result}; -use gtk::prelude::*; -use gtk::{Button, IconTheme, Label, Orientation}; -use serde::Deserialize; -use tokio::spawn; -use tokio::sync::mpsc::{Receiver, Sender}; -use tracing::{debug, error}; - -#[derive(Debug, Deserialize, Clone)] -pub struct CustomModule { - /// Container class name - class: Option, - /// Widgets to add to the bar container - bar: Vec, - /// Widgets to add to the popup container - popup: Option>, - - #[serde(flatten)] - pub common: Option, -} - -/// Attempts to parse an `Orientation` from `String` -fn try_get_orientation(orientation: &str) -> Result { - match orientation.to_lowercase().as_str() { - "horizontal" | "h" => Ok(Orientation::Horizontal), - "vertical" | "v" => Ok(Orientation::Vertical), - _ => Err(Report::msg("Invalid orientation string in config")), - } -} - -/// Widget attributes -#[derive(Debug, Deserialize, Clone)] -pub struct Widget { - /// Type of GTK widget to add - #[serde(rename = "type")] - widget_type: WidgetType, - widgets: Option>, - label: Option, - name: Option, - class: Option, - on_click: Option, - orientation: Option, - src: Option, - size: Option, -} - -/// Supported GTK widget types -#[derive(Debug, Deserialize, Clone)] -#[serde(rename_all = "snake_case")] -pub enum WidgetType { - Box, - Label, - Button, - Image, -} - -impl Widget { - /// Creates this widget and adds it to the parent container - fn add_to( - self, - parent: >k::Box, - tx: Sender, - bar_orientation: Orientation, - icon_theme: &IconTheme, - ) { - match self.widget_type { - WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation, icon_theme)), - WidgetType::Label => parent.add(&self.into_label()), - WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)), - WidgetType::Image => parent.add(&self.into_image(icon_theme)), - } - } - - /// Creates a `gtk::Box` from this widget - fn into_box( - self, - tx: &Sender, - bar_orientation: Orientation, - icon_theme: &IconTheme, - ) -> gtk::Box { - let mut builder = gtk::Box::builder(); - - if let Some(name) = self.name { - builder = builder.name(&name); - } - - if let Some(orientation) = self.orientation { - builder = builder - .orientation(try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal)); - } - - let container = builder.build(); - - if let Some(class) = self.class { - container.style_context().add_class(&class); - } - - if let Some(widgets) = self.widgets { - for widget in widgets { - widget.add_to(&container, tx.clone(), bar_orientation, icon_theme); - } - } - - container - } - - /// Creates a `gtk::Label` from this widget - fn into_label(self) -> Label { - let mut builder = Label::builder().use_markup(true); - - if let Some(name) = self.name { - builder = builder.name(name); - } - - let label = builder.build(); - - if let Some(class) = self.class { - label.style_context().add_class(&class); - } - - let text = self.label.map_or_else(String::new, |text| text); - - { - let label = label.clone(); - DynamicString::new(&text, move |string| { - label.set_label(&string); - Continue(true) - }); - } - - label - } - - /// Creates a `gtk::Button` from this widget - fn into_button(self, tx: Sender, bar_orientation: Orientation) -> Button { - let mut builder = Button::builder(); - - if let Some(name) = self.name { - builder = builder.name(name); - } - - let button = builder.build(); - - if let Some(text) = self.label { - let label = Label::new(None); - label.set_use_markup(true); - label.set_markup(&text); - button.add(&label); - } - - if let Some(class) = self.class { - button.style_context().add_class(&class); - } - - if let Some(exec) = self.on_click { - button.connect_clicked(move |button| { - try_send!( - tx, - ExecEvent { - cmd: exec.clone(), - geometry: Popup::button_pos(button, bar_orientation), - } - ); - }); - } - - button - } - - fn into_image(self, icon_theme: &IconTheme) -> gtk::Image { - let mut builder = gtk::Image::builder(); - - if let Some(name) = self.name { - builder = builder.name(&name); - } - - let gtk_image = builder.build(); - - if let Some(src) = self.src { - let size = self.size.unwrap_or(32); - if let Err(err) = ImageProvider::parse(&src, icon_theme, size) - .and_then(|image| image.load_into_image(gtk_image.clone())) - { - error!("{err:?}"); - } - } - - if let Some(class) = self.class { - gtk_image.style_context().add_class(&class); - } - - gtk_image - } -} - -#[derive(Debug)] -pub struct ExecEvent { - cmd: String, - geometry: ButtonGeometry, -} - -impl Module for CustomModule { - type SendMessage = (); - type ReceiveMessage = ExecEvent; - - fn name() -> &'static str { - "custom" - } - - fn spawn_controller( - &self, - _info: &ModuleInfo, - tx: Sender>, - mut rx: Receiver, - ) -> Result<()> { - spawn(async move { - while let Some(event) = rx.recv().await { - if event.cmd.starts_with('!') { - let script = Script::from(&event.cmd[1..]); - - debug!("executing command: '{}'", script.cmd); - // TODO: Migrate to use script.run - if let Err(err) = script.get_output().await { - error!("{err:?}"); - } - } else if event.cmd == "popup:toggle" { - send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry)); - } else if event.cmd == "popup:open" { - send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry)); - } else if event.cmd == "popup:close" { - send_async!(tx, ModuleUpdateEvent::ClosePopup); - } else { - error!("Received invalid command: '{}'", event.cmd); - } - } - }); - - Ok(()) - } - - fn into_widget( - self, - context: WidgetContext, - info: &ModuleInfo, - ) -> Result> { - let orientation = info.bar_position.get_orientation(); - let container = gtk::Box::builder().orientation(orientation).build(); - - if let Some(ref class) = self.class { - container.style_context().add_class(class); - } - - self.bar.clone().into_iter().for_each(|widget| { - widget.add_to( - &container, - context.controller_tx.clone(), - orientation, - info.icon_theme, - ); - }); - - let popup = self.into_popup(context.controller_tx, context.popup_rx, info); - - Ok(ModuleWidget { - widget: container, - popup, - }) - } - - fn into_popup( - self, - tx: Sender, - _rx: glib::Receiver, - info: &ModuleInfo, - ) -> Option - where - Self: Sized, - { - let container = gtk::Box::builder().name("popup-custom").build(); - - if let Some(class) = self.class { - container - .style_context() - .add_class(format!("popup-{class}").as_str()); - } - - if let Some(popup) = self.popup { - for widget in popup { - widget.add_to( - &container, - tx.clone(), - Orientation::Horizontal, - info.icon_theme, - ); - } - } - - container.show_all(); - - Some(container) - } -} diff --git a/src/modules/custom/box.rs b/src/modules/custom/box.rs new file mode 100644 index 0000000..6ea0748 --- /dev/null +++ b/src/modules/custom/box.rs @@ -0,0 +1,43 @@ +use super::{try_get_orientation, CustomWidget, CustomWidgetContext, Widget}; +use gtk::prelude::*; +use gtk::Orientation; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct BoxWidget { + name: Option, + class: Option, + orientation: Option, + widgets: Option>, +} + +impl CustomWidget for BoxWidget { + type Widget = gtk::Box; + + fn into_widget(self, context: CustomWidgetContext) -> Self::Widget { + let mut builder = gtk::Box::builder(); + + if let Some(name) = self.name { + builder = builder.name(&name); + } + + if let Some(orientation) = self.orientation { + builder = builder + .orientation(try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal)); + } + + let container = builder.build(); + + if let Some(class) = self.class { + container.style_context().add_class(&class); + } + + if let Some(widgets) = self.widgets { + for widget in widgets { + widget.add_to(&container, context); + } + } + + container + } +} diff --git a/src/modules/custom/button.rs b/src/modules/custom/button.rs new file mode 100644 index 0000000..e688234 --- /dev/null +++ b/src/modules/custom/button.rs @@ -0,0 +1,56 @@ +use super::{CustomWidget, CustomWidgetContext, ExecEvent}; +use crate::popup::Popup; +use crate::try_send; +use gtk::prelude::*; +use gtk::{Button, Label}; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct ButtonWidget { + name: Option, + class: Option, + label: Option, + on_click: Option, +} + +impl CustomWidget for ButtonWidget { + type Widget = Button; + + fn into_widget(self, context: CustomWidgetContext) -> Self::Widget { + let mut builder = Button::builder(); + + if let Some(name) = self.name { + builder = builder.name(name); + } + + let button = builder.build(); + + if let Some(text) = self.label { + let label = Label::new(None); + label.set_use_markup(true); + label.set_markup(&text); + button.add(&label); + } + + if let Some(class) = self.class { + button.style_context().add_class(&class); + } + + if let Some(exec) = self.on_click { + let bar_orientation = context.bar_orientation; + let tx = context.tx.clone(); + + button.connect_clicked(move |button| { + try_send!( + tx, + ExecEvent { + cmd: exec.clone(), + geometry: Popup::button_pos(button, bar_orientation), + } + ); + }); + } + + button + } +} diff --git a/src/modules/custom/image.rs b/src/modules/custom/image.rs new file mode 100644 index 0000000..ba56638 --- /dev/null +++ b/src/modules/custom/image.rs @@ -0,0 +1,43 @@ +use super::{CustomWidget, CustomWidgetContext}; +use crate::image::ImageProvider; +use gtk::prelude::*; +use gtk::Image; +use serde::Deserialize; +use tracing::error; + +#[derive(Debug, Deserialize, Clone)] +pub struct ImageWidget { + name: Option, + class: Option, + src: Option, + size: Option, +} + +impl CustomWidget for ImageWidget { + type Widget = Image; + + fn into_widget(self, context: CustomWidgetContext) -> Self::Widget { + let mut builder = Image::builder(); + + if let Some(name) = self.name { + builder = builder.name(&name); + } + + let gtk_image = builder.build(); + + if let Some(src) = self.src { + let size = self.size.unwrap_or(32); + if let Err(err) = ImageProvider::parse(&src, context.icon_theme, size) + .and_then(|image| image.load_into_image(gtk_image.clone())) + { + error!("{err:?}"); + } + } + + if let Some(class) = self.class { + gtk_image.style_context().add_class(&class); + } + + gtk_image + } +} diff --git a/src/modules/custom/label.rs b/src/modules/custom/label.rs new file mode 100644 index 0000000..9badc5d --- /dev/null +++ b/src/modules/custom/label.rs @@ -0,0 +1,42 @@ +use super::{CustomWidget, CustomWidgetContext}; +use crate::dynamic_string::DynamicString; +use gtk::prelude::*; +use gtk::Label; +use serde::Deserialize; + +#[derive(Debug, Deserialize, Clone)] +pub struct LabelWidget { + name: Option, + class: Option, + label: Option, +} + +impl CustomWidget for LabelWidget { + type Widget = Label; + + fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget { + let mut builder = Label::builder().use_markup(true); + + if let Some(name) = self.name { + builder = builder.name(name); + } + + let label = builder.build(); + + if let Some(class) = self.class { + label.style_context().add_class(&class); + } + + let text = self.label.map_or_else(String::new, |text| text); + + { + let label = label.clone(); + DynamicString::new(&text, move |string| { + label.set_label(&string); + Continue(true) + }); + } + + label + } +} diff --git a/src/modules/custom/mod.rs b/src/modules/custom/mod.rs new file mode 100644 index 0000000..e929d7b --- /dev/null +++ b/src/modules/custom/mod.rs @@ -0,0 +1,187 @@ +mod r#box; +mod button; +mod image; +mod label; + +use self::image::ImageWidget; +use self::label::LabelWidget; +use self::r#box::BoxWidget; +use crate::config::CommonConfig; +use crate::modules::custom::button::ButtonWidget; +use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; +use crate::popup::ButtonGeometry; +use crate::script::Script; +use crate::send_async; +use color_eyre::{Report, Result}; +use gtk::prelude::*; +use gtk::{IconTheme, Orientation}; +use serde::Deserialize; +use tokio::spawn; +use tokio::sync::mpsc::{Receiver, Sender}; +use tracing::{debug, error}; + +#[derive(Debug, Deserialize, Clone)] +pub struct CustomModule { + /// Container class name + class: Option, + /// Widgets to add to the bar container + bar: Vec, + /// Widgets to add to the popup container + popup: Option>, + + #[serde(flatten)] + pub common: Option, +} + +/// Attempts to parse an `Orientation` from `String` +fn try_get_orientation(orientation: &str) -> Result { + match orientation.to_lowercase().as_str() { + "horizontal" | "h" => Ok(Orientation::Horizontal), + "vertical" | "v" => Ok(Orientation::Vertical), + _ => Err(Report::msg("Invalid orientation string in config")), + } +} + +#[derive(Debug, Deserialize, Clone)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Widget { + Box(BoxWidget), + Label(LabelWidget), + Button(ButtonWidget), + Image(ImageWidget) +} + +#[derive(Clone, Copy)] +struct CustomWidgetContext<'a> { + tx: &'a Sender, + bar_orientation: Orientation, + icon_theme: &'a IconTheme, +} + +trait CustomWidget { + type Widget; + + fn into_widget(self, context: CustomWidgetContext) -> Self::Widget; +} + +impl Widget { + /// Creates this widget and adds it to the parent container + fn add_to(self, parent: >k::Box, context: CustomWidgetContext) { + match self { + Widget::Box(widget) => parent.add(&widget.into_widget(context)), + Widget::Label(widget) => parent.add(&widget.into_widget(context)), + Widget::Button(widget) => parent.add(&widget.into_widget(context)), + Widget::Image(widget) => parent.add(&widget.into_widget(context)), + } + } +} + +#[derive(Debug)] +pub struct ExecEvent { + cmd: String, + geometry: ButtonGeometry, +} + +impl Module for CustomModule { + type SendMessage = (); + type ReceiveMessage = ExecEvent; + + fn name() -> &'static str { + "custom" + } + + fn spawn_controller( + &self, + _info: &ModuleInfo, + tx: Sender>, + mut rx: Receiver, + ) -> Result<()> { + spawn(async move { + while let Some(event) = rx.recv().await { + if event.cmd.starts_with('!') { + let script = Script::from(&event.cmd[1..]); + + debug!("executing command: '{}'", script.cmd); + // TODO: Migrate to use script.run + if let Err(err) = script.get_output().await { + error!("{err:?}"); + } + } else if event.cmd == "popup:toggle" { + send_async!(tx, ModuleUpdateEvent::TogglePopup(event.geometry)); + } else if event.cmd == "popup:open" { + send_async!(tx, ModuleUpdateEvent::OpenPopup(event.geometry)); + } else if event.cmd == "popup:close" { + send_async!(tx, ModuleUpdateEvent::ClosePopup); + } else { + error!("Received invalid command: '{}'", event.cmd); + } + } + }); + + Ok(()) + } + + fn into_widget( + self, + context: WidgetContext, + info: &ModuleInfo, + ) -> Result> { + let orientation = info.bar_position.get_orientation(); + let container = gtk::Box::builder().orientation(orientation).build(); + + if let Some(ref class) = self.class { + container.style_context().add_class(class); + } + + let custom_context = CustomWidgetContext { + tx: &context.controller_tx, + bar_orientation: orientation, + icon_theme: info.icon_theme, + }; + + self.bar.clone().into_iter().for_each(|widget| { + widget.add_to(&container, custom_context); + }); + + let popup = self.into_popup(context.controller_tx, context.popup_rx, info); + + Ok(ModuleWidget { + widget: container, + popup, + }) + } + + fn into_popup( + self, + tx: Sender, + _rx: glib::Receiver, + info: &ModuleInfo, + ) -> Option + where + Self: Sized, + { + let container = gtk::Box::builder().name("popup-custom").build(); + + if let Some(class) = self.class { + container + .style_context() + .add_class(format!("popup-{class}").as_str()); + } + + if let Some(popup) = self.popup { + let custom_context = CustomWidgetContext { + tx: &tx, + bar_orientation: info.bar_position.get_orientation(), + icon_theme: info.icon_theme, + }; + + for widget in popup { + widget.add_to(&container, custom_context); + } + } + + container.show_all(); + + Some(container) + } +} From e928b30f9927aa7c895c0d9855ee3ef09e559dc7 Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sun, 9 Apr 2023 22:31:29 +0100 Subject: [PATCH 2/7] docs(custom): rewrite widget options to be clearer --- docs/modules/Custom.md | 119 ++++++++++++++++++++++++++++------------- 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/docs/modules/Custom.md b/docs/modules/Custom.md index 0d0184e..7ec0001 100644 --- a/docs/modules/Custom.md +++ b/docs/modules/Custom.md @@ -1,7 +1,7 @@ Allows you to compose custom modules consisting of multiple widgets, including popups. 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://f.jstanger.dev/github/ironbar/custom-power-menu.png) +![Custom module with a button on the bar, and the popup open. The popup contains a header, shutdown button and restart button.](https://f.jstanger.dev/github/ironbar/custom-power-menu.png?raw) ## Configuration @@ -10,29 +10,67 @@ Labels can display dynamic content from scripts, and buttons can interact with t This module can be quite fiddly to configure as you effectively have to build a tree of widgets by hand. It is well worth looking at the examples. -| Name | Type | Default | Description | -|---------|------------|---------|--------------------------------------| -| `class` | `string` | `null` | Container class name. | -| `bar` | `Widget[]` | `null` | List of widgets to add to the bar. | -| `popup` | `Widget[]` | `[]` | List of widgets to add to the popup. | - ### `Widget` -| Name | Type | Default | Description | -|---------------|-----------------------------------------|--------------|---------------------------------------------------------------------------| -| `widget_type` | `box` or `label` or `button` or `image` | `null` | Type of GTK widget to create. | -| `name` | `string` | `null` | Widget name. | -| `class` | `string` | `null` | Widget class name. | -| `label` | `string` | `null` | [`label` and `button`] Widget text label. Pango markup supported. | -| `on_click` | `string` | `null` | [`button`] Command to execute. More on this [below](#commands). | -| `src` | `image` | `null` | [`image`] Image source. See [here](images) for information on images. | -| `size` | `integer` | `null` | [`image`] Width/height of the image. Aspect ratio is preserved. | -| `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. | +There are many widget types, each with their own config options. +You can think of these like HTML elements and their attributes. -### Labels +Every widget has the following options available; `type` is mandatory. + +| Name | Type | Default | Description | +|---------|-----------------------------------------|---------|-------------------------------| +| `type` | `box` or `label` or `button` or `image` | `null` | Type of GTK widget to create. | +| `name` | `string` | `null` | Widget name. | +| `class` | `string` | `null` | Widget class name. | + +#### Box + +A container to place nested widgets inside. + +> Type: `box` + +| Name | Type | Default | Description | +|---------------|----------------------------------------------------|--------------|-------------------------------------------------------------------| +| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Whether child widgets should be horizontally or vertically added. | +| `widgets` | `Widget[]` | `[]` | List of widgets to add to this box. | + +#### Label + +A text label. Pango markup and embedded scripts are supported. + +> Type `label` + +| Name | Type | Default | Description | +|---------|----------|--------------|---------------------------------------------------------------------| +| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. | + +#### Button + +A clickable button, which can run a command when clicked. + +> Type `button` + +| Name | Type | Default | Description | +|------------|----------|--------------|---------------------------------------------------------------------| +| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. | +| `on_click` | `string` | `null` | Command to execute. More on this [below](#commands). | + +#### Image + +An image or icon from disk or http. + +> Type `image` + +| Name | Type | Default | Description | +|--------|-----------|---------|-------------------------------------------------------------| +| `src` | `image` | `null` | Image source. See [here](images) for information on images. | +| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. | + +### Label Attributes + +Any widgets with a `label` attribute support embedded scripts, +meaning you can interpolate text from scripts to dynamically show content. -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. @@ -238,27 +276,32 @@ end: ```corn let { + $button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } + + $popup = { + type = "box" + orientation = "vertical" + widgets = [ + { type = "label" name = "header" label = "Power menu" } + { + type = "box" + widgets = [ + { 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-}}" } + ] + } + $power_menu = { type = "custom" class = "power-menu" - bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ] + bar = [ $button ] + popup = [ $popup ] - popup = [ { - type = "box" - orientation = "vertical" - widgets = [ - { type = "label" name = "header" label = "Power menu" } - { - type = "box" - widgets = [ - { 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-}}" } - ] - } ] + tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}" } } in { end = [ $power_menu ] @@ -269,7 +312,9 @@ let { ## Styling -Since the widgets are all custom, you can target them using `#name` and `.class`. +Since the widgets are all custom, you can use the `name` and `class` attributes, then target them using `#name` and `.class`. + +The following top-level selector is always available: | Selector | Description | |-----------|-------------------------| From dfe1964abf9ca54beb38cad0bcf02bd9fb0b5c4d Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Sun, 9 Apr 2023 22:42:35 +0100 Subject: [PATCH 3/7] feat(custom): slider widget Resolves partially #68. --- docs/modules/Custom.md | 64 ++++++++++++++--- src/bar.rs | 4 +- src/dynamic_string.rs | 2 +- src/modules/clipboard.rs | 2 +- src/modules/clock.rs | 2 +- src/modules/custom/button.rs | 3 +- src/modules/custom/mod.rs | 17 +++-- src/modules/custom/slider.rs | 131 +++++++++++++++++++++++++++++++++++ src/modules/launcher/item.rs | 2 +- src/modules/mod.rs | 6 +- src/modules/music/mod.rs | 2 +- src/modules/script.rs | 2 +- src/popup.rs | 35 +++++----- src/script.rs | 22 ++++-- 14 files changed, 245 insertions(+), 49 deletions(-) create mode 100644 src/modules/custom/slider.rs diff --git a/docs/modules/Custom.md b/docs/modules/Custom.md index 7ec0001..fc4332d 100644 --- a/docs/modules/Custom.md +++ b/docs/modules/Custom.md @@ -17,11 +17,11 @@ You can think of these like HTML elements and their attributes. Every widget has the following options available; `type` is mandatory. -| Name | Type | Default | Description | -|---------|-----------------------------------------|---------|-------------------------------| -| `type` | `box` or `label` or `button` or `image` | `null` | Type of GTK widget to create. | -| `name` | `string` | `null` | Widget name. | -| `class` | `string` | `null` | Widget class name. | +| Name | Type | Default | Description | +|---------|-----------------------------------------------------|---------|-------------------------------| +| `type` | `box` or `label` or `button` or `image` or `slider` | `null` | Type of GTK widget to create. | +| `name` | `string` | `null` | Widget name. | +| `class` | `string` | `null` | Widget class name. | #### Box @@ -50,10 +50,10 @@ A clickable button, which can run a command when clicked. > Type `button` -| Name | Type | Default | Description | -|------------|----------|--------------|---------------------------------------------------------------------| -| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. | -| `on_click` | `string` | `null` | Command to execute. More on this [below](#commands). | +| Name | Type | Default | Description | +|------------|--------------------|--------------|---------------------------------------------------------------------| +| `label` | `string` | `horizontal` | Widget text label. Pango markup and embedded scripts are supported. | +| `on_click` | `string [command]` | `null` | Command to execute. More on this [below](#commands). | #### Image @@ -66,8 +66,51 @@ An image or icon from disk or http. | `src` | `image` | `null` | Image source. See [here](images) for information on images. | | `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. | +#### Slider + +A draggable slider. + +> Type: `slider` + +Note that `on_change` will provide the **floating point** value as an argument. +If your input program requires an integer, you will need to round it. + +| Name | Type | Default | Description | +|---------------|----------------------------------------------------|--------------|------------------------------------------------------------------------------| +| `src` | `image` | `null` | Image source. See [here](images) for information on images. | +| `size` | `integer` | `null` | Width/height of the image. Aspect ratio is preserved. | +| `orientation` | `horizontal` or `vertical` (shorthand: `h` or `v`) | `horizontal` | Orientation of the slider. | +| `value` | `Script` | `null` | Script to run to get the slider value. Output must be a valid number. | +| `on_change` | `string [command]` | `null` | Command to execute when the slider changes. More on this [below](#commands). | +| `min` | `float` | `0` | Minimum slider value. | +| `max` | `float` | `100` | Maximum slider value. | +| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. | + +Note that `on_change` will provide the **floating point** value as an argument. +If your input program requires an integer, you will need to round it. + +The example slider widget below shows a volume control for MPC, +which updates the server when changed, and polls the server for volume changes to keep the slider in sync. + +```corn +$slider = { + type = "custom" + bar = [ + { + type = "slider" + length = 100 + max = 100 + on_change="!mpc volume ${0%.*}" + value = "200:mpc volume | cut -d ':' -f2 | cut -d '%' -f1" + } + ] +} +``` + ### Label Attributes +> ℹ This is different to the `label` widget, although applies to it. + Any widgets with a `label` attribute support embedded scripts, meaning you can interpolate text from scripts to dynamically show content. @@ -90,6 +133,9 @@ 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 `on_click` to `!~/.local/bin/my-script.sh`. +Some widgets provide a value when they run the command, such as `slider`. +This is passed as an argument and can be accessed using `$0`. + The following bar commands are supported: - `popup:toggle` diff --git a/src/bar.rs b/src/bar.rs index fbb126e..98e82b7 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -373,7 +373,7 @@ fn setup_module_common_options(container: EventBox, common: CommonConfig) { let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); spawn(async move { script - .run(|(_, success)| { + .run(None, |_, success| { send!(tx, success); }) .await; @@ -456,7 +456,7 @@ fn setup_module_common_options(container: EventBox, common: CommonConfig) { } fn run_script(script: &Script) { - match await_sync(async { script.get_output().await }) { + match await_sync(async { script.get_output(None).await }) { Ok((OutputStream::Stderr(out), _)) => error!("{out}"), Err(err) => error!("{err:?}"), _ => {} diff --git a/src/dynamic_string.rs b/src/dynamic_string.rs index b3f7b79..43c7e89 100644 --- a/src/dynamic_string.rs +++ b/src/dynamic_string.rs @@ -79,7 +79,7 @@ impl DynamicString { spawn(async move { script - .run(|(out, _)| { + .run(None, |out, _| { if let OutputStream::Stdout(out) = out { let mut label_parts = lock!(label_parts); diff --git a/src/modules/clipboard.rs b/src/modules/clipboard.rs index 0357228..8307fe5 100644 --- a/src/modules/clipboard.rs +++ b/src/modules/clipboard.rs @@ -124,7 +124,7 @@ impl Module