mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-07-02 03:01:04 +02:00
Merge pull request #104 from JakeStanger/feat/custom-widgets
Custom module improvements
This commit is contained in:
commit
b770ae716c
19 changed files with 859 additions and 421 deletions
|
@ -1,7 +1,7 @@
|
||||||
Allows you to compose custom modules consisting of multiple widgets, including popups.
|
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.
|
Labels can display dynamic content from scripts, and buttons can interact with the bar or execute commands on click.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
@ -10,29 +10,141 @@ 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.
|
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.
|
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`
|
### `Widget`
|
||||||
|
|
||||||
|
There are many widget types, each with their own config options.
|
||||||
|
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 |
|
| Name | Type | Default | Description |
|
||||||
|---------------|-----------------------------------------|--------------|---------------------------------------------------------------------------|
|
|---------|-------------------------------------------------------------------|---------|-------------------------------|
|
||||||
| `widget_type` | `box` or `label` or `button` or `image` | `null` | Type of GTK widget to create. |
|
| `type` | `box` or `label` or `button` or `image` or `slider` or `progress` | `null` | Type of GTK widget to create. |
|
||||||
| `name` | `string` | `null` | Widget name. |
|
| `name` | `string` | `null` | Widget name. |
|
||||||
| `class` | `string` | `null` | Widget class 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. |
|
|
||||||
|
|
||||||
### Labels
|
#### 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 [command]` | `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. |
|
||||||
|
|
||||||
|
#### 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. |
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Progress
|
||||||
|
|
||||||
|
A progress bar.
|
||||||
|
|
||||||
|
> Type: `progress`
|
||||||
|
|
||||||
|
Note that `value` expects a numeric value **between 0-`max`** as output.
|
||||||
|
|
||||||
|
| 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 progress bar value. Output must be a valid percentage. |
|
||||||
|
| `max` | `float` | `100` | Maximum progress bar value. |
|
||||||
|
| `length` | `integer` | `null` | Slider length. GTK will automatically size if left unset. |
|
||||||
|
|
||||||
|
The example below shows progress for the current playing song in MPD,
|
||||||
|
and displays the elapsed/length timestamps as a label above:
|
||||||
|
|
||||||
|
```corn
|
||||||
|
$progress = {
|
||||||
|
type = "custom"
|
||||||
|
bar = [
|
||||||
|
{
|
||||||
|
type = "progress"
|
||||||
|
value = "500:mpc | sed -n 2p | awk '{ print $4 }' | grep -Eo '[0-9]+'"
|
||||||
|
label = "{{500:mpc | sed -n 2p | awk '{ print $3 }'}} elapsed"
|
||||||
|
length = 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
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.
|
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.
|
For example, the following label would output your system uptime, updated every 30 seconds.
|
||||||
|
@ -52,6 +164,9 @@ To execute shell commands, prefix them with an `!`.
|
||||||
For example, if you want to run `~/.local/bin/my-script.sh` on click,
|
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`.
|
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:
|
The following bar commands are supported:
|
||||||
|
|
||||||
- `popup:toggle`
|
- `popup:toggle`
|
||||||
|
@ -238,13 +353,9 @@ end:
|
||||||
|
|
||||||
```corn
|
```corn
|
||||||
let {
|
let {
|
||||||
$power_menu = {
|
$button = { type = "button" name="power-btn" label = "" on_click = "popup:toggle" }
|
||||||
type = "custom"
|
|
||||||
class = "power-menu"
|
|
||||||
|
|
||||||
bar = [ { type = "button" name="power-btn" label = "" on_click = "popup:toggle" } ]
|
$popup = {
|
||||||
|
|
||||||
popup = [ {
|
|
||||||
type = "box"
|
type = "box"
|
||||||
orientation = "vertical"
|
orientation = "vertical"
|
||||||
widgets = [
|
widgets = [
|
||||||
|
@ -258,7 +369,16 @@ let {
|
||||||
}
|
}
|
||||||
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||||
]
|
]
|
||||||
} ]
|
}
|
||||||
|
|
||||||
|
$power_menu = {
|
||||||
|
type = "custom"
|
||||||
|
class = "power-menu"
|
||||||
|
|
||||||
|
bar = [ $button ]
|
||||||
|
popup = [ $popup ]
|
||||||
|
|
||||||
|
tooltip = "Up: {{30000:uptime -p | cut -d ' ' -f2-}}"
|
||||||
}
|
}
|
||||||
} in {
|
} in {
|
||||||
end = [ $power_menu ]
|
end = [ $power_menu ]
|
||||||
|
@ -269,7 +389,9 @@ let {
|
||||||
|
|
||||||
## Styling
|
## 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 |
|
| Selector | Description |
|
||||||
|-----------|-------------------------|
|
|-----------|-------------------------|
|
||||||
|
|
|
@ -373,7 +373,7 @@ fn setup_module_common_options(container: EventBox, common: CommonConfig) {
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
script
|
script
|
||||||
.run(|(_, success)| {
|
.run(None, |_, success| {
|
||||||
send!(tx, success);
|
send!(tx, success);
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -456,7 +456,7 @@ fn setup_module_common_options(container: EventBox, common: CommonConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_script(script: &Script) {
|
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}"),
|
Ok((OutputStream::Stderr(out), _)) => error!("{out}"),
|
||||||
Err(err) => error!("{err:?}"),
|
Err(err) => error!("{err:?}"),
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
|
@ -4,6 +4,9 @@ use gtk::prelude::*;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
|
|
||||||
|
/// A segment of a dynamic string,
|
||||||
|
/// containing either a static string
|
||||||
|
/// or a script.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum DynamicStringSegment {
|
enum DynamicStringSegment {
|
||||||
Static(String),
|
Static(String),
|
||||||
|
@ -16,51 +19,20 @@ pub struct DynamicString;
|
||||||
impl DynamicString {
|
impl DynamicString {
|
||||||
/// Creates a new dynamic string, based off the input template.
|
/// Creates a new dynamic string, based off the input template.
|
||||||
/// Runs `f` with the compiled string each time one of the scripts updates.
|
/// Runs `f` with the compiled string each time one of the scripts updates.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rs
|
||||||
|
/// DynamicString::new(&text, move |string| {
|
||||||
|
/// label.set_markup(&string);
|
||||||
|
/// Continue(true)
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
pub fn new<F>(input: &str, f: F) -> Self
|
pub fn new<F>(input: &str, f: F) -> Self
|
||||||
where
|
where
|
||||||
F: FnMut(String) -> Continue + 'static,
|
F: FnMut(String) -> Continue + 'static,
|
||||||
{
|
{
|
||||||
let mut segments = vec![];
|
let segments = Self::parse_input(input);
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
(
|
|
||||||
DynamicStringSegment::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();
|
|
||||||
|
|
||||||
(DynamicStringSegment::Static(str), len)
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_ne!(skip, 0);
|
|
||||||
|
|
||||||
segments.push(token);
|
|
||||||
chars.drain(..skip);
|
|
||||||
}
|
|
||||||
|
|
||||||
let label_parts = Arc::new(Mutex::new(Vec::new()));
|
let label_parts = Arc::new(Mutex::new(Vec::new()));
|
||||||
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
@ -79,7 +51,7 @@ impl DynamicString {
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
script
|
script
|
||||||
.run(|(out, _)| {
|
.run(None, |out, _| {
|
||||||
if let OutputStream::Stdout(out) = out {
|
if let OutputStream::Stdout(out) = out {
|
||||||
let mut label_parts = lock!(label_parts);
|
let mut label_parts = lock!(label_parts);
|
||||||
|
|
||||||
|
@ -105,6 +77,62 @@ impl DynamicString {
|
||||||
|
|
||||||
Self
|
Self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses the input string into static and dynamic segments
|
||||||
|
fn parse_input(input: &str) -> Vec<DynamicStringSegment> {
|
||||||
|
if !input.contains("{{") {
|
||||||
|
return vec![DynamicStringSegment::Static(input.to_string())];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut segments = vec![];
|
||||||
|
|
||||||
|
let mut chars = input.chars().collect::<Vec<_>>();
|
||||||
|
while !chars.is_empty() {
|
||||||
|
let char_pair = &chars[..=1];
|
||||||
|
|
||||||
|
let (token, skip) = if let ['{', '{'] = char_pair {
|
||||||
|
const SKIP_BRACKETS: usize = 4; // two braces either side
|
||||||
|
|
||||||
|
let str = chars
|
||||||
|
.windows(2)
|
||||||
|
.skip(2)
|
||||||
|
.take_while(|win| win != &['}', '}'])
|
||||||
|
.map(|w| w[0])
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let len = str.len();
|
||||||
|
|
||||||
|
(
|
||||||
|
DynamicStringSegment::Dynamic(Script::from(str.as_str())),
|
||||||
|
len + SKIP_BRACKETS,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let mut str = chars
|
||||||
|
.windows(2)
|
||||||
|
.take_while(|win| win != &['{', '{'])
|
||||||
|
.map(|w| w[0])
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
// if segment is at end of string, last char gets missed above due to uneven window.
|
||||||
|
if chars.len() == str.len() + 1 {
|
||||||
|
let remaining_char = *chars.get(str.len()).expect("Failed to find last char");
|
||||||
|
str.push(remaining_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = str.len();
|
||||||
|
|
||||||
|
(DynamicStringSegment::Static(str), len)
|
||||||
|
};
|
||||||
|
|
||||||
|
// quick runtime check to make sure the parser is working as expected
|
||||||
|
assert_ne!(skip, 0);
|
||||||
|
|
||||||
|
segments.push(token);
|
||||||
|
chars.drain(..skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
segments
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -124,7 +124,7 @@ impl Module<Button> for ClipboardModule {
|
||||||
button.style_context().add_class("btn");
|
button.style_context().add_class("btn");
|
||||||
|
|
||||||
button.connect_clicked(move |button| {
|
button.connect_clicked(move |button| {
|
||||||
let pos = Popup::button_pos(button, position.get_orientation());
|
let pos = Popup::widget_geometry(button, position.get_orientation());
|
||||||
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
|
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ impl Module<Button> for ClockModule {
|
||||||
button.connect_clicked(move |button| {
|
button.connect_clicked(move |button| {
|
||||||
try_send!(
|
try_send!(
|
||||||
context.tx,
|
context.tx,
|
||||||
ModuleUpdateEvent::TogglePopup(Popup::button_pos(button, orientation))
|
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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<String>,
|
|
||||||
/// Widgets to add to the bar container
|
|
||||||
bar: Vec<Widget>,
|
|
||||||
/// Widgets to add to the popup container
|
|
||||||
popup: Option<Vec<Widget>>,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub common: Option<CommonConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to parse an `Orientation` from `String`
|
|
||||||
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
|
|
||||||
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<Vec<Widget>>,
|
|
||||||
label: Option<String>,
|
|
||||||
name: Option<String>,
|
|
||||||
class: Option<String>,
|
|
||||||
on_click: Option<String>,
|
|
||||||
orientation: Option<String>,
|
|
||||||
src: Option<String>,
|
|
||||||
size: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<ExecEvent>,
|
|
||||||
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<ExecEvent>,
|
|
||||||
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<ExecEvent>, 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<gtk::Box> for CustomModule {
|
|
||||||
type SendMessage = ();
|
|
||||||
type ReceiveMessage = ExecEvent;
|
|
||||||
|
|
||||||
fn name() -> &'static str {
|
|
||||||
"custom"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_controller(
|
|
||||||
&self,
|
|
||||||
_info: &ModuleInfo,
|
|
||||||
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
|
||||||
mut rx: Receiver<Self::ReceiveMessage>,
|
|
||||||
) -> 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<Self::SendMessage, Self::ReceiveMessage>,
|
|
||||||
info: &ModuleInfo,
|
|
||||||
) -> Result<ModuleWidget<gtk::Box>> {
|
|
||||||
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<Self::ReceiveMessage>,
|
|
||||||
_rx: glib::Receiver<Self::SendMessage>,
|
|
||||||
info: &ModuleInfo,
|
|
||||||
) -> Option<gtk::Box>
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
35
src/modules/custom/box.rs
Normal file
35
src/modules/custom/box.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, Widget};
|
||||||
|
use crate::build;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::Orientation;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct BoxWidget {
|
||||||
|
name: Option<String>,
|
||||||
|
class: Option<String>,
|
||||||
|
orientation: Option<String>,
|
||||||
|
widgets: Option<Vec<Widget>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomWidget for BoxWidget {
|
||||||
|
type Widget = gtk::Box;
|
||||||
|
|
||||||
|
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||||
|
let container = build!(self, Self::Widget);
|
||||||
|
|
||||||
|
if let Some(orientation) = self.orientation {
|
||||||
|
container.set_orientation(
|
||||||
|
try_get_orientation(&orientation).unwrap_or(Orientation::Horizontal),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(widgets) = self.widgets {
|
||||||
|
for widget in widgets {
|
||||||
|
widget.add_to(&container, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container
|
||||||
|
}
|
||||||
|
}
|
52
src/modules/custom/button.rs
Normal file
52
src/modules/custom/button.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
||||||
|
use crate::dynamic_string::DynamicString;
|
||||||
|
use crate::popup::Popup;
|
||||||
|
use crate::{build, try_send};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{Button, Label};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ButtonWidget {
|
||||||
|
name: Option<String>,
|
||||||
|
class: Option<String>,
|
||||||
|
label: Option<String>,
|
||||||
|
on_click: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomWidget for ButtonWidget {
|
||||||
|
type Widget = Button;
|
||||||
|
|
||||||
|
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||||
|
let button = build!(self, Self::Widget);
|
||||||
|
|
||||||
|
if let Some(text) = self.label {
|
||||||
|
let label = Label::new(None);
|
||||||
|
label.set_use_markup(true);
|
||||||
|
button.add(&label);
|
||||||
|
|
||||||
|
DynamicString::new(&text, move |string| {
|
||||||
|
label.set_markup(&string);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
args: None,
|
||||||
|
geometry: Popup::widget_geometry(button, bar_orientation),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
button
|
||||||
|
}
|
||||||
|
}
|
34
src/modules/custom/image.rs
Normal file
34
src/modules/custom/image.rs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
use super::{CustomWidget, CustomWidgetContext};
|
||||||
|
use crate::build;
|
||||||
|
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<String>,
|
||||||
|
class: Option<String>,
|
||||||
|
src: Option<String>,
|
||||||
|
size: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomWidget for ImageWidget {
|
||||||
|
type Widget = Image;
|
||||||
|
|
||||||
|
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||||
|
let gtk_image = build!(self, Self::Widget);
|
||||||
|
|
||||||
|
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:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_image
|
||||||
|
}
|
||||||
|
}
|
33
src/modules/custom/label.rs
Normal file
33
src/modules/custom/label.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
use super::{CustomWidget, CustomWidgetContext};
|
||||||
|
use crate::build;
|
||||||
|
use crate::dynamic_string::DynamicString;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::Label;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct LabelWidget {
|
||||||
|
name: Option<String>,
|
||||||
|
class: Option<String>,
|
||||||
|
label: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomWidget for LabelWidget {
|
||||||
|
type Widget = Label;
|
||||||
|
|
||||||
|
fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget {
|
||||||
|
let label = build!(self, Self::Widget);
|
||||||
|
|
||||||
|
label.set_use_markup(true);
|
||||||
|
|
||||||
|
{
|
||||||
|
let label = label.clone();
|
||||||
|
DynamicString::new(&self.label, move |string| {
|
||||||
|
label.set_markup(&string);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
label
|
||||||
|
}
|
||||||
|
}
|
233
src/modules/custom/mod.rs
Normal file
233
src/modules/custom/mod.rs
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
mod r#box;
|
||||||
|
mod button;
|
||||||
|
mod image;
|
||||||
|
mod label;
|
||||||
|
mod progress;
|
||||||
|
mod slider;
|
||||||
|
|
||||||
|
use self::image::ImageWidget;
|
||||||
|
use self::label::LabelWidget;
|
||||||
|
use self::r#box::BoxWidget;
|
||||||
|
use self::slider::SliderWidget;
|
||||||
|
use crate::config::CommonConfig;
|
||||||
|
use crate::modules::custom::button::ButtonWidget;
|
||||||
|
use crate::modules::custom::progress::ProgressWidget;
|
||||||
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||||
|
use crate::popup::WidgetGeometry;
|
||||||
|
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<String>,
|
||||||
|
/// Widgets to add to the bar container
|
||||||
|
bar: Vec<Widget>,
|
||||||
|
/// Widgets to add to the popup container
|
||||||
|
popup: Option<Vec<Widget>>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub common: Option<CommonConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum Widget {
|
||||||
|
Box(BoxWidget),
|
||||||
|
Label(LabelWidget),
|
||||||
|
Button(ButtonWidget),
|
||||||
|
Image(ImageWidget),
|
||||||
|
Slider(SliderWidget),
|
||||||
|
Progress(ProgressWidget),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct CustomWidgetContext<'a> {
|
||||||
|
tx: &'a Sender<ExecEvent>,
|
||||||
|
bar_orientation: Orientation,
|
||||||
|
icon_theme: &'a IconTheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
trait CustomWidget {
|
||||||
|
type Widget;
|
||||||
|
|
||||||
|
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new widget of type `ty`,
|
||||||
|
/// setting its name and class based on
|
||||||
|
/// the values available on `self`.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! build {
|
||||||
|
($self:ident, $ty:ty) => {{
|
||||||
|
let mut builder = <$ty>::builder();
|
||||||
|
|
||||||
|
if let Some(name) = &$self.name {
|
||||||
|
builder = builder.name(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
let widget = builder.build();
|
||||||
|
|
||||||
|
if let Some(class) = &$self.class {
|
||||||
|
widget.style_context().add_class(class);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the widget length,
|
||||||
|
/// using either a width or height request
|
||||||
|
/// based on the bar's orientation.
|
||||||
|
pub fn set_length<W: WidgetExt>(widget: &W, length: i32, bar_orientation: Orientation) {
|
||||||
|
match bar_orientation {
|
||||||
|
Orientation::Horizontal => widget.set_width_request(length),
|
||||||
|
Orientation::Vertical => widget.set_height_request(length),
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to parse an `Orientation` from `String`.
|
||||||
|
/// Will accept `horizontal`, `vertical`, `h` or `v`.
|
||||||
|
/// Ignores case.
|
||||||
|
fn try_get_orientation(orientation: &str) -> Result<Orientation> {
|
||||||
|
match orientation.to_lowercase().as_str() {
|
||||||
|
"horizontal" | "h" => Ok(Orientation::Horizontal),
|
||||||
|
"vertical" | "v" => Ok(Orientation::Vertical),
|
||||||
|
_ => Err(Report::msg("Invalid orientation string in config")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)),
|
||||||
|
Widget::Slider(widget) => parent.add(&widget.into_widget(context)),
|
||||||
|
Widget::Progress(widget) => parent.add(&widget.into_widget(context)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ExecEvent {
|
||||||
|
cmd: String,
|
||||||
|
args: Option<Vec<String>>,
|
||||||
|
geometry: WidgetGeometry,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module<gtk::Box> for CustomModule {
|
||||||
|
type SendMessage = ();
|
||||||
|
type ReceiveMessage = ExecEvent;
|
||||||
|
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_controller(
|
||||||
|
&self,
|
||||||
|
_info: &ModuleInfo,
|
||||||
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
||||||
|
mut rx: Receiver<Self::ReceiveMessage>,
|
||||||
|
) -> 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);
|
||||||
|
|
||||||
|
let args = event.args.unwrap_or(vec![]);
|
||||||
|
|
||||||
|
if let Err(err) = script.get_output(Some(&args)).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<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleWidget<gtk::Box>> {
|
||||||
|
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<Self::ReceiveMessage>,
|
||||||
|
_rx: glib::Receiver<Self::SendMessage>,
|
||||||
|
info: &ModuleInfo,
|
||||||
|
) -> Option<gtk::Box>
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
80
src/modules/custom/progress.rs
Normal file
80
src/modules/custom/progress.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
use super::{try_get_orientation, CustomWidget, CustomWidgetContext};
|
||||||
|
use crate::dynamic_string::DynamicString;
|
||||||
|
use crate::modules::custom::set_length;
|
||||||
|
use crate::script::{OutputStream, Script, ScriptInput};
|
||||||
|
use crate::{build, send};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::ProgressBar;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct ProgressWidget {
|
||||||
|
name: Option<String>,
|
||||||
|
class: Option<String>,
|
||||||
|
orientation: Option<String>,
|
||||||
|
label: Option<String>,
|
||||||
|
value: Option<ScriptInput>,
|
||||||
|
#[serde(default = "default_max")]
|
||||||
|
max: f64,
|
||||||
|
length: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_max() -> f64 {
|
||||||
|
100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomWidget for ProgressWidget {
|
||||||
|
type Widget = ProgressBar;
|
||||||
|
|
||||||
|
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||||
|
let progress = build!(self, Self::Widget);
|
||||||
|
|
||||||
|
if let Some(orientation) = self.orientation {
|
||||||
|
progress.set_orientation(
|
||||||
|
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(length) = self.length {
|
||||||
|
set_length(&progress, length, context.bar_orientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = self.value {
|
||||||
|
let script = Script::from(value);
|
||||||
|
let progress = progress.clone();
|
||||||
|
|
||||||
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
script
|
||||||
|
.run(None, move |stream, _success| match stream {
|
||||||
|
OutputStream::Stdout(out) => match out.parse::<f64>() {
|
||||||
|
Ok(value) => send!(tx, value),
|
||||||
|
Err(err) => error!("{err:?}"),
|
||||||
|
},
|
||||||
|
OutputStream::Stderr(err) => error!("{err:?}"),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
rx.attach(None, move |value| {
|
||||||
|
progress.set_fraction(value / self.max);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(text) = self.label {
|
||||||
|
let progress = progress.clone();
|
||||||
|
progress.set_show_text(true);
|
||||||
|
|
||||||
|
DynamicString::new(&text, move |string| {
|
||||||
|
progress.set_text(Some(&string));
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
progress
|
||||||
|
}
|
||||||
|
}
|
119
src/modules/custom/slider.rs
Normal file
119
src/modules/custom/slider.rs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
use super::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
|
||||||
|
use crate::modules::custom::set_length;
|
||||||
|
use crate::popup::Popup;
|
||||||
|
use crate::script::{OutputStream, Script, ScriptInput};
|
||||||
|
use crate::{build, send, try_send};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::Scale;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::cell::Cell;
|
||||||
|
use tokio::spawn;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct SliderWidget {
|
||||||
|
name: Option<String>,
|
||||||
|
class: Option<String>,
|
||||||
|
orientation: Option<String>,
|
||||||
|
value: Option<ScriptInput>,
|
||||||
|
on_change: Option<String>,
|
||||||
|
#[serde(default = "default_min")]
|
||||||
|
min: f64,
|
||||||
|
#[serde(default = "default_max")]
|
||||||
|
max: f64,
|
||||||
|
length: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_min() -> f64 {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_max() -> f64 {
|
||||||
|
100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomWidget for SliderWidget {
|
||||||
|
type Widget = Scale;
|
||||||
|
|
||||||
|
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||||
|
let scale = build!(self, Self::Widget);
|
||||||
|
|
||||||
|
if let Some(orientation) = self.orientation {
|
||||||
|
scale.set_orientation(
|
||||||
|
try_get_orientation(&orientation).unwrap_or(context.bar_orientation),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(length) = self.length {
|
||||||
|
set_length(&scale, length, context.bar_orientation);
|
||||||
|
}
|
||||||
|
|
||||||
|
scale.set_range(self.min, self.max);
|
||||||
|
|
||||||
|
if let Some(on_change) = self.on_change {
|
||||||
|
let min = self.min;
|
||||||
|
let max = self.max;
|
||||||
|
let tx = context.tx.clone();
|
||||||
|
|
||||||
|
// GTK will spam the same value over and over
|
||||||
|
let prev_value = Cell::new(scale.value());
|
||||||
|
|
||||||
|
scale.connect_change_value(move |scale, _, val| {
|
||||||
|
// GTK will send values outside min/max range
|
||||||
|
let val = clamp(val, min, max);
|
||||||
|
|
||||||
|
if val != prev_value.get() {
|
||||||
|
try_send!(
|
||||||
|
tx,
|
||||||
|
ExecEvent {
|
||||||
|
cmd: on_change.clone(),
|
||||||
|
args: Some(vec![val.to_string()]),
|
||||||
|
geometry: Popup::widget_geometry(scale, context.bar_orientation),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
prev_value.set(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
Inhibit(false)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(value) = self.value {
|
||||||
|
let script = Script::from(value);
|
||||||
|
let scale = scale.clone();
|
||||||
|
|
||||||
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
script
|
||||||
|
.run(None, move |stream, _success| match stream {
|
||||||
|
OutputStream::Stdout(out) => match out.parse() {
|
||||||
|
Ok(value) => send!(tx, value),
|
||||||
|
Err(err) => error!("{err:?}"),
|
||||||
|
},
|
||||||
|
OutputStream::Stderr(err) => error!("{err:?}"),
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
rx.attach(None, move |value| {
|
||||||
|
scale.set_value(value);
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensures `num` is between `min` and `max` (inclusive).
|
||||||
|
fn clamp(num: f64, min: f64, max: f64) -> f64 {
|
||||||
|
if num < min {
|
||||||
|
min
|
||||||
|
} else if num > max {
|
||||||
|
max
|
||||||
|
} else {
|
||||||
|
num
|
||||||
|
}
|
||||||
|
}
|
|
@ -217,7 +217,7 @@ impl ItemButton {
|
||||||
|
|
||||||
try_send!(
|
try_send!(
|
||||||
tx,
|
tx,
|
||||||
ModuleUpdateEvent::OpenPopup(Popup::button_pos(button, orientation,))
|
ModuleUpdateEvent::OpenPopup(Popup::widget_geometry(button, orientation,))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||||
|
|
|
@ -23,7 +23,7 @@ pub mod tray;
|
||||||
pub mod workspaces;
|
pub mod workspaces;
|
||||||
|
|
||||||
use crate::config::BarPosition;
|
use crate::config::BarPosition;
|
||||||
use crate::popup::ButtonGeometry;
|
use crate::popup::WidgetGeometry;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use glib::IsA;
|
use glib::IsA;
|
||||||
use gtk::gdk::Monitor;
|
use gtk::gdk::Monitor;
|
||||||
|
@ -50,10 +50,10 @@ pub enum ModuleUpdateEvent<T> {
|
||||||
/// Sends an update to the module UI
|
/// Sends an update to the module UI
|
||||||
Update(T),
|
Update(T),
|
||||||
/// Toggles the open state of the popup.
|
/// Toggles the open state of the popup.
|
||||||
TogglePopup(ButtonGeometry),
|
TogglePopup(WidgetGeometry),
|
||||||
/// Force sets the popup open.
|
/// Force sets the popup open.
|
||||||
/// Takes the button X position and width.
|
/// Takes the button X position and width.
|
||||||
OpenPopup(ButtonGeometry),
|
OpenPopup(WidgetGeometry),
|
||||||
/// Force sets the popup closed.
|
/// Force sets the popup closed.
|
||||||
ClosePopup,
|
ClosePopup,
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,7 +179,7 @@ impl Module<Button> for MusicModule {
|
||||||
button.connect_clicked(move |button| {
|
button.connect_clicked(move |button| {
|
||||||
try_send!(
|
try_send!(
|
||||||
tx,
|
tx,
|
||||||
ModuleUpdateEvent::TogglePopup(Popup::button_pos(button, orientation,))
|
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation,))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ impl Module<Label> for ScriptModule {
|
||||||
let script: Script = self.into();
|
let script: Script = self.into();
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
script.run(move |(out, _)| match out {
|
script.run(None, move |out, _| match out {
|
||||||
OutputStream::Stdout(stdout) => {
|
OutputStream::Stdout(stdout) => {
|
||||||
try_send!(tx, ModuleUpdateEvent::Update(stdout));
|
try_send!(tx, ModuleUpdateEvent::Update(stdout));
|
||||||
},
|
},
|
||||||
|
|
35
src/popup.rs
35
src/popup.rs
|
@ -4,7 +4,7 @@ use crate::config::BarPosition;
|
||||||
use crate::modules::ModuleInfo;
|
use crate::modules::ModuleInfo;
|
||||||
use gtk::gdk::Monitor;
|
use gtk::gdk::Monitor;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{ApplicationWindow, Button, Orientation};
|
use gtk::{ApplicationWindow, Orientation};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -133,7 +133,7 @@ impl Popup {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the popup
|
/// Shows the popup
|
||||||
pub fn show(&self, geometry: ButtonGeometry) {
|
pub fn show(&self, geometry: WidgetGeometry) {
|
||||||
self.window.show();
|
self.window.show();
|
||||||
self.set_pos(geometry);
|
self.set_pos(geometry);
|
||||||
}
|
}
|
||||||
|
@ -150,7 +150,7 @@ impl Popup {
|
||||||
|
|
||||||
/// Sets the popup's X/Y position relative to the left or border of the screen
|
/// Sets the popup's X/Y position relative to the left or border of the screen
|
||||||
/// (depending on orientation).
|
/// (depending on orientation).
|
||||||
fn set_pos(&self, geometry: ButtonGeometry) {
|
fn set_pos(&self, geometry: WidgetGeometry) {
|
||||||
let orientation = self.pos.get_orientation();
|
let orientation = self.pos.get_orientation();
|
||||||
|
|
||||||
let mon_workarea = self.monitor.workarea();
|
let mon_workarea = self.monitor.workarea();
|
||||||
|
@ -190,14 +190,17 @@ impl Popup {
|
||||||
|
|
||||||
/// Gets the absolute X position of the button
|
/// Gets the absolute X position of the button
|
||||||
/// and its width / height (depending on orientation).
|
/// and its width / height (depending on orientation).
|
||||||
pub fn button_pos(button: &Button, orientation: Orientation) -> ButtonGeometry {
|
pub fn widget_geometry<W>(widget: &W, orientation: Orientation) -> WidgetGeometry
|
||||||
let button_size = if orientation == Orientation::Horizontal {
|
where
|
||||||
button.allocation().width()
|
W: IsA<gtk::Widget>,
|
||||||
|
{
|
||||||
|
let widget_size = if orientation == Orientation::Horizontal {
|
||||||
|
widget.allocation().width()
|
||||||
} else {
|
} else {
|
||||||
button.allocation().height()
|
widget.allocation().height()
|
||||||
};
|
};
|
||||||
|
|
||||||
let top_level = button.toplevel().expect("Failed to get top-level widget");
|
let top_level = widget.toplevel().expect("Failed to get top-level widget");
|
||||||
|
|
||||||
let bar_size = if orientation == Orientation::Horizontal {
|
let bar_size = if orientation == Orientation::Horizontal {
|
||||||
top_level.allocation().width()
|
top_level.allocation().width()
|
||||||
|
@ -205,26 +208,26 @@ impl Popup {
|
||||||
top_level.allocation().height()
|
top_level.allocation().height()
|
||||||
};
|
};
|
||||||
|
|
||||||
let (button_x, button_y) = button
|
let (widget_x, widget_y) = widget
|
||||||
.translate_coordinates(&top_level, 0, 0)
|
.translate_coordinates(&top_level, 0, 0)
|
||||||
.unwrap_or((0, 0));
|
.unwrap_or((0, 0));
|
||||||
|
|
||||||
let button_pos = if orientation == Orientation::Horizontal {
|
let widget_pos = if orientation == Orientation::Horizontal {
|
||||||
button_x
|
widget_x
|
||||||
} else {
|
} else {
|
||||||
button_y
|
widget_y
|
||||||
};
|
};
|
||||||
|
|
||||||
ButtonGeometry {
|
WidgetGeometry {
|
||||||
position: button_pos,
|
position: widget_pos,
|
||||||
size: button_size,
|
size: widget_size,
|
||||||
bar_size,
|
bar_size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct ButtonGeometry {
|
pub struct WidgetGeometry {
|
||||||
position: i32,
|
position: i32,
|
||||||
size: i32,
|
size: i32,
|
||||||
bar_size: i32,
|
bar_size: i32,
|
||||||
|
|
|
@ -180,20 +180,22 @@ impl Script {
|
||||||
script
|
script
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run<F>(&self, callback: F)
|
/// Runs the script, passing `args` if provided.
|
||||||
|
/// Runs `f`, passing the output stream and whether the command returned 0.
|
||||||
|
pub async fn run<F>(&self, args: Option<&[String]>, callback: F)
|
||||||
where
|
where
|
||||||
F: Fn((OutputStream, bool)),
|
F: Fn(OutputStream, bool),
|
||||||
{
|
{
|
||||||
loop {
|
loop {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
ScriptMode::Poll => match self.get_output().await {
|
ScriptMode::Poll => match self.get_output(args).await {
|
||||||
Ok(output) => callback(output),
|
Ok(output) => callback(output.0, output.1),
|
||||||
Err(err) => error!("{err:?}"),
|
Err(err) => error!("{err:?}"),
|
||||||
},
|
},
|
||||||
ScriptMode::Watch => match self.spawn().await {
|
ScriptMode::Watch => match self.spawn().await {
|
||||||
Ok(mut rx) => {
|
Ok(mut rx) => {
|
||||||
while let Some(msg) = rx.recv().await {
|
while let Some(msg) = rx.recv().await {
|
||||||
callback((msg, true));
|
callback(msg, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => error!("{err:?}"),
|
Err(err) => error!("{err:?}"),
|
||||||
|
@ -210,9 +212,15 @@ impl Script {
|
||||||
/// the `stdout` is returned.
|
/// the `stdout` is returned.
|
||||||
/// Otherwise, an `Err` variant
|
/// Otherwise, an `Err` variant
|
||||||
/// containing the `stderr` is returned.
|
/// containing the `stderr` is returned.
|
||||||
pub async fn get_output(&self) -> Result<(OutputStream, bool)> {
|
pub async fn get_output(&self, args: Option<&[String]>) -> Result<(OutputStream, bool)> {
|
||||||
|
let mut args_list = vec!["-c", &self.cmd];
|
||||||
|
|
||||||
|
if let Some(args) = args {
|
||||||
|
args_list.extend(args.iter().map(|s| s.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
let output = Command::new("sh")
|
let output = Command::new("sh")
|
||||||
.args(["-c", &self.cmd])
|
.args(&args_list)
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.wrap_err("Failed to get script output")?;
|
.wrap_err("Failed to get script output")?;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue