mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-07-01 10:41:03 +02:00
feat(custom): ability to embed scripts in labels for dynamic content
Fully resolves #34.
This commit is contained in:
parent
e274ba39cd
commit
5d153a02fc
6 changed files with 196 additions and 33 deletions
|
@ -1,5 +1,5 @@
|
||||||
Allows you to compose custom modules consisting of multiple widgets, including popups.
|
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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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
|
### Commands
|
||||||
|
|
||||||
Buttons can execute commands that interact with the bar,
|
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 `!`.
|
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 `exec` to `!~/.local/bin/my-script.sh`.
|
you'd set `on_click` to `!~/.local/bin/my-script.sh`.
|
||||||
|
|
||||||
The following bar commands are supported:
|
The following bar commands are supported:
|
||||||
|
|
||||||
|
@ -44,7 +57,7 @@ The following bar commands are supported:
|
||||||
- `popup:close`
|
- `popup:close`
|
||||||
|
|
||||||
XML is arguably better-suited and easier to read for this sort of markup,
|
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
|
Nonetheless, it may be worth comparing the examples to the below equivalent
|
||||||
to help get your head around what's going on:
|
to help get your head around what's going on:
|
||||||
|
|
||||||
|
@ -53,15 +66,16 @@ to help get your head around what's going on:
|
||||||
<?xml version="1.0" encoding="utf-8" ?>
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
<custom class="power-menu">
|
<custom class="power-menu">
|
||||||
<bar>
|
<bar>
|
||||||
<button name="power-btn" label="" exec="popup:toggle"/>
|
<button name="power-btn" label="" on_click="popup:toggle"/>
|
||||||
</bar>
|
</bar>
|
||||||
<popup>
|
<popup>
|
||||||
<box orientation="vertical">
|
<box orientation="vertical">
|
||||||
<label name="header" label="Power menu" />
|
<label name="header" label="Power menu" />
|
||||||
<box>
|
<box>
|
||||||
<button class="power-btn" label="" exec="!shutdown now" />
|
<button class="power-btn" label="" on_click="!shutdown now" />
|
||||||
<button class="power-btn" label="" exec="!reboot" />
|
<button class="power-btn" label="" on_click="!reboot" />
|
||||||
</box>
|
</box>
|
||||||
|
<label name="uptime" label="Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" />
|
||||||
</box>
|
</box>
|
||||||
</popup>
|
</popup>
|
||||||
</custom>
|
</custom>
|
||||||
|
@ -74,10 +88,12 @@ to help get your head around what's going on:
|
||||||
{
|
{
|
||||||
"end": [
|
"end": [
|
||||||
{
|
{
|
||||||
"type": "custom",
|
"type": "clock"
|
||||||
|
},
|
||||||
|
{
|
||||||
"bar": [
|
"bar": [
|
||||||
{
|
{
|
||||||
"exec": "popup:toggle",
|
"on_click": "popup:toggle",
|
||||||
"label": "",
|
"label": "",
|
||||||
"name": "power-btn",
|
"name": "power-btn",
|
||||||
"type": "button"
|
"type": "button"
|
||||||
|
@ -99,21 +115,27 @@ to help get your head around what's going on:
|
||||||
"widgets": [
|
"widgets": [
|
||||||
{
|
{
|
||||||
"class": "power-btn",
|
"class": "power-btn",
|
||||||
"exec": "!shutdown now",
|
"on_click": "!shutdown now",
|
||||||
"label": "<span font-size='40pt'></span>",
|
"label": "<span font-size='40pt'></span>",
|
||||||
"type": "button"
|
"type": "button"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"class": "power-btn",
|
"class": "power-btn",
|
||||||
"exec": "!reboot",
|
"on_click": "!reboot",
|
||||||
"label": "<span font-size='40pt'></span>",
|
"label": "<span font-size='40pt'></span>",
|
||||||
"type": "button"
|
"type": "button"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}",
|
||||||
|
"name": "uptime",
|
||||||
|
"type": "label"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"type": "custom"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -125,12 +147,15 @@ to help get your head around what's going on:
|
||||||
<summary>TOML</summary>
|
<summary>TOML</summary>
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
[[end]]
|
||||||
|
type = 'clock'
|
||||||
|
|
||||||
[[end]]
|
[[end]]
|
||||||
class = 'power-menu'
|
class = 'power-menu'
|
||||||
type = 'custom'
|
type = 'custom'
|
||||||
|
|
||||||
[[end.bar]]
|
[[end.bar]]
|
||||||
exec = 'popup:toggle'
|
on_click = 'popup:toggle'
|
||||||
label = ''
|
label = ''
|
||||||
name = 'power-btn'
|
name = 'power-btn'
|
||||||
type = 'button'
|
type = 'button'
|
||||||
|
@ -149,15 +174,20 @@ type = 'box'
|
||||||
|
|
||||||
[[end.popup.widgets.widgets]]
|
[[end.popup.widgets.widgets]]
|
||||||
class = 'power-btn'
|
class = 'power-btn'
|
||||||
exec = '!shutdown now'
|
on_click = '!shutdown now'
|
||||||
label = '''<span font-size='40pt'></span>'''
|
label = '''<span font-size='40pt'></span>'''
|
||||||
type = 'button'
|
type = 'button'
|
||||||
|
|
||||||
[[end.popup.widgets.widgets]]
|
[[end.popup.widgets.widgets]]
|
||||||
class = 'power-btn'
|
class = 'power-btn'
|
||||||
exec = '!reboot'
|
on_click = '!reboot'
|
||||||
label = '''<span font-size='40pt'></span>'''
|
label = '''<span font-size='40pt'></span>'''
|
||||||
type = 'button'
|
type = 'button'
|
||||||
|
|
||||||
|
[[end.popup.widgets]]
|
||||||
|
label = '''Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}'''
|
||||||
|
name = 'uptime'
|
||||||
|
type = 'label'
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
@ -167,9 +197,9 @@ type = 'button'
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
end:
|
end:
|
||||||
- type: custom
|
- type: clock
|
||||||
bar:
|
- bar:
|
||||||
- exec: popup:toggle
|
- on_click: popup:toggle
|
||||||
label:
|
label:
|
||||||
name: power-btn
|
name: power-btn
|
||||||
type: button
|
type: button
|
||||||
|
@ -184,13 +214,17 @@ end:
|
||||||
- type: box
|
- type: box
|
||||||
widgets:
|
widgets:
|
||||||
- class: power-btn
|
- class: power-btn
|
||||||
exec: '!shutdown now'
|
on_click: '!shutdown now'
|
||||||
label: <span font-size='40pt'></span>
|
label: <span font-size='40pt'></span>
|
||||||
type: button
|
type: button
|
||||||
- class: power-btn
|
- class: power-btn
|
||||||
exec: '!reboot'
|
on_click: '!reboot'
|
||||||
label: <span font-size='40pt'></span>
|
label: <span font-size='40pt'></span>
|
||||||
type: button
|
type: button
|
||||||
|
- label: 'Uptime: {{30000:uptime -p | cut -d '' '' -f2-}}'
|
||||||
|
name: uptime
|
||||||
|
type: label
|
||||||
|
type: custom
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
@ -204,7 +238,7 @@ let {
|
||||||
type = "custom"
|
type = "custom"
|
||||||
class = "power-menu"
|
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 = [ {
|
popup = [ {
|
||||||
type = "box"
|
type = "box"
|
||||||
|
@ -214,10 +248,11 @@ let {
|
||||||
{
|
{
|
||||||
type = "box"
|
type = "box"
|
||||||
widgets = [
|
widgets = [
|
||||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" exec = "!shutdown now" }
|
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!shutdown now" }
|
||||||
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" exec = "!reboot" }
|
{ type = "button" class="power-btn" label = "<span font-size='40pt'></span>" on_click = "!reboot" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
{ type = "label" name = "uptime" label = "Uptime: {{30000:uptime -p | cut -d ' ' -f2-}}" }
|
||||||
]
|
]
|
||||||
} ]
|
} ]
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ pub struct CommonConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub enum ModuleConfig {
|
pub enum ModuleConfig {
|
||||||
Clock(ClockModule),
|
Clock(ClockModule),
|
||||||
Mpd(MpdModule),
|
Mpd(MpdModule),
|
||||||
|
@ -48,7 +48,7 @@ pub enum MonitorConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Deserialize, Copy, Clone, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum BarPosition {
|
pub enum BarPosition {
|
||||||
Top,
|
Top,
|
||||||
Bottom,
|
Bottom,
|
||||||
|
|
|
@ -8,6 +8,7 @@ mod modules;
|
||||||
mod popup;
|
mod popup;
|
||||||
mod script;
|
mod script;
|
||||||
mod style;
|
mod style;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
use crate::bar::create_bar;
|
use crate::bar::create_bar;
|
||||||
use crate::config::{Config, MonitorConfig};
|
use crate::config::{Config, MonitorConfig};
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::config::CommonConfig;
|
||||||
use color_eyre::{Report, Result};
|
use color_eyre::{Report, Result};
|
||||||
use crate::script::Script;
|
use crate::script::Script;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
use crate::widgets::DynamicLabel;
|
||||||
use gtk::{Button, Label, Orientation};
|
use gtk::{Button, Label, Orientation};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::spawn;
|
use tokio::spawn;
|
||||||
|
@ -48,7 +49,7 @@ pub struct Widget {
|
||||||
|
|
||||||
/// Supported GTK widget types
|
/// Supported GTK widget types
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum WidgetType {
|
pub enum WidgetType {
|
||||||
Box,
|
Box,
|
||||||
Label,
|
Label,
|
||||||
|
@ -60,7 +61,7 @@ impl Widget {
|
||||||
fn add_to(self, parent: >k::Box, tx: Sender<ExecEvent>, bar_orientation: Orientation) {
|
fn add_to(self, parent: >k::Box, tx: Sender<ExecEvent>, bar_orientation: Orientation) {
|
||||||
match self.widget_type {
|
match self.widget_type {
|
||||||
WidgetType::Box => parent.add(&self.into_box(&tx, bar_orientation)),
|
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)),
|
WidgetType::Button => parent.add(&self.into_button(tx, bar_orientation)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +95,7 @@ impl Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `gtk::Label` from this 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);
|
let mut builder = Label::builder().use_markup(true);
|
||||||
|
|
||||||
if let Some(name) = self.name {
|
if let Some(name) = self.name {
|
||||||
|
@ -103,15 +104,13 @@ impl Widget {
|
||||||
|
|
||||||
let label = builder.build();
|
let label = builder.build();
|
||||||
|
|
||||||
if let Some(text) = self.label {
|
|
||||||
label.set_markup(&text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(class) = self.class {
|
if let Some(class) = self.class {
|
||||||
label.style_context().add_class(&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
|
/// Creates a `gtk::Button` from this widget
|
||||||
|
|
125
src/widgets/dynamic_label.rs
Normal file
125
src/widgets/dynamic_label.rs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
use crate::script::{OutputStream, Script};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::spawn;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum DynamicLabelSegment {
|
||||||
|
Static(String),
|
||||||
|
Dynamic(Script),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DynamicLabel {
|
||||||
|
pub label: gtk::Label,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DynamicLabel {
|
||||||
|
pub fn new(label: gtk::Label, input: &str) -> Self {
|
||||||
|
let mut segments = vec![];
|
||||||
|
|
||||||
|
let mut chars = input.chars().collect::<Vec<_>>();
|
||||||
|
while !chars.is_empty() {
|
||||||
|
let char = &chars[..=1];
|
||||||
|
|
||||||
|
let (token, skip) = if let ['{', '{'] = char {
|
||||||
|
const SKIP_BRACKETS: usize = 4;
|
||||||
|
|
||||||
|
let str = chars
|
||||||
|
.iter()
|
||||||
|
.skip(2)
|
||||||
|
.enumerate()
|
||||||
|
.take_while(|(i, &c)| c != '}' && chars[i + 1] != '}')
|
||||||
|
.map(|(_, c)| c)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let len = str.len();
|
||||||
|
|
||||||
|
(
|
||||||
|
DynamicLabelSegment::Dynamic(Script::from(str.as_str())),
|
||||||
|
len + SKIP_BRACKETS,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let str = chars
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.take_while(|(i, &c)| !(c == '{' && chars[i + 1] == '{'))
|
||||||
|
.map(|(_, c)| c)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let len = str.len();
|
||||||
|
|
||||||
|
(DynamicLabelSegment::Static(str), len)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_ne!(skip, 0);
|
||||||
|
|
||||||
|
segments.push(token);
|
||||||
|
chars.drain(..skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label_parts = Arc::new(Mutex::new(IndexMap::new()));
|
||||||
|
let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
for (i, segment) in segments.into_iter().enumerate() {
|
||||||
|
match segment {
|
||||||
|
DynamicLabelSegment::Static(str) => {
|
||||||
|
label_parts
|
||||||
|
.lock()
|
||||||
|
.expect("Failed to get lock on label parts")
|
||||||
|
.insert(i, str);
|
||||||
|
}
|
||||||
|
DynamicLabelSegment::Dynamic(script) => {
|
||||||
|
let tx = tx.clone();
|
||||||
|
let label_parts = label_parts.clone();
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
script
|
||||||
|
.run(|(out, _)| {
|
||||||
|
if let OutputStream::Stdout(out) = out {
|
||||||
|
label_parts
|
||||||
|
.lock()
|
||||||
|
.expect("Failed to get lock on label parts")
|
||||||
|
.insert(i, out);
|
||||||
|
tx.send(()).expect("Failed to send update");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.send(()).expect("Failed to send update");
|
||||||
|
|
||||||
|
{
|
||||||
|
let label = label.clone();
|
||||||
|
rx.attach(None, move |_| {
|
||||||
|
let new_label = label_parts
|
||||||
|
.lock()
|
||||||
|
.expect("Failed to get lock on label parts")
|
||||||
|
.iter()
|
||||||
|
.map(|(_, part)| part.as_str())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
label.set_label(new_label.as_str());
|
||||||
|
|
||||||
|
Continue(true)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { label }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test() {
|
||||||
|
gtk::init().unwrap();
|
||||||
|
let label = gtk::Label::new(None);
|
||||||
|
DynamicLabel::new(label, "Uptime: {{1000:uptime -p | cut -d ' ' -f2-}}");
|
||||||
|
}
|
||||||
|
}
|
3
src/widgets/mod.rs
Normal file
3
src/widgets/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod dynamic_label;
|
||||||
|
|
||||||
|
pub use dynamic_label::DynamicLabel;
|
Loading…
Add table
Add a link
Reference in a new issue