mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-04-19 19:34:24 +02:00
parent
e928b30f99
commit
dfe1964abf
14 changed files with 245 additions and 49 deletions
|
@ -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`
|
||||
|
|
|
@ -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:?}"),
|
||||
_ => {}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ impl Module<Button> for ClipboardModule {
|
|||
button.style_context().add_class("btn");
|
||||
|
||||
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));
|
||||
});
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ impl Module<Button> for ClockModule {
|
|||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
context.tx,
|
||||
ModuleUpdateEvent::TogglePopup(Popup::button_pos(button, orientation))
|
||||
ModuleUpdateEvent::TogglePopup(Popup::widget_geometry(button, orientation))
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -45,7 +45,8 @@ impl CustomWidget for ButtonWidget {
|
|||
tx,
|
||||
ExecEvent {
|
||||
cmd: exec.clone(),
|
||||
geometry: Popup::button_pos(button, bar_orientation),
|
||||
args: None,
|
||||
geometry: Popup::widget_geometry(button, bar_orientation),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -2,14 +2,16 @@ mod r#box;
|
|||
mod button;
|
||||
mod image;
|
||||
mod label;
|
||||
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::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
||||
use crate::popup::ButtonGeometry;
|
||||
use crate::popup::WidgetGeometry;
|
||||
use crate::script::Script;
|
||||
use crate::send_async;
|
||||
use color_eyre::{Report, Result};
|
||||
|
@ -48,7 +50,8 @@ pub enum Widget {
|
|||
Box(BoxWidget),
|
||||
Label(LabelWidget),
|
||||
Button(ButtonWidget),
|
||||
Image(ImageWidget)
|
||||
Image(ImageWidget),
|
||||
Slider(SliderWidget),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
@ -72,6 +75,7 @@ impl Widget {
|
|||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +83,8 @@ impl Widget {
|
|||
#[derive(Debug)]
|
||||
pub struct ExecEvent {
|
||||
cmd: String,
|
||||
geometry: ButtonGeometry,
|
||||
args: Option<Vec<String>>,
|
||||
geometry: WidgetGeometry,
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for CustomModule {
|
||||
|
@ -102,8 +107,10 @@ impl Module<gtk::Box> for CustomModule {
|
|||
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 {
|
||||
|
||||
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" {
|
||||
|
|
131
src/modules/custom/slider.rs
Normal file
131
src/modules/custom/slider.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
use crate::modules::custom::{try_get_orientation, CustomWidget, CustomWidgetContext, ExecEvent};
|
||||
use crate::popup::Popup;
|
||||
use crate::script::{OutputStream, Script, ScriptInput};
|
||||
use crate::{send, try_send};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Orientation, 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 mut builder = Scale::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(context.bar_orientation));
|
||||
}
|
||||
|
||||
if let Some(length) = self.length {
|
||||
builder = match context.bar_orientation {
|
||||
Orientation::Horizontal => builder.width_request(length),
|
||||
Orientation::Vertical => builder.height_request(length),
|
||||
_ => builder,
|
||||
}
|
||||
}
|
||||
|
||||
let scale = builder.build();
|
||||
|
||||
scale.set_range(self.min, self.max);
|
||||
|
||||
if let Some(class) = self.class {
|
||||
scale.style_context().add_class(&class);
|
||||
}
|
||||
|
||||
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!(
|
||||
tx,
|
||||
ModuleUpdateEvent::OpenPopup(Popup::button_pos(button, orientation,))
|
||||
ModuleUpdateEvent::OpenPopup(Popup::widget_geometry(button, orientation,))
|
||||
);
|
||||
} else {
|
||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
|
|
|
@ -23,7 +23,7 @@ pub mod tray;
|
|||
pub mod workspaces;
|
||||
|
||||
use crate::config::BarPosition;
|
||||
use crate::popup::ButtonGeometry;
|
||||
use crate::popup::WidgetGeometry;
|
||||
use color_eyre::Result;
|
||||
use glib::IsA;
|
||||
use gtk::gdk::Monitor;
|
||||
|
@ -50,10 +50,10 @@ pub enum ModuleUpdateEvent<T> {
|
|||
/// Sends an update to the module UI
|
||||
Update(T),
|
||||
/// Toggles the open state of the popup.
|
||||
TogglePopup(ButtonGeometry),
|
||||
TogglePopup(WidgetGeometry),
|
||||
/// Force sets the popup open.
|
||||
/// Takes the button X position and width.
|
||||
OpenPopup(ButtonGeometry),
|
||||
OpenPopup(WidgetGeometry),
|
||||
/// Force sets the popup closed.
|
||||
ClosePopup,
|
||||
}
|
||||
|
|
|
@ -179,7 +179,7 @@ impl Module<Button> for MusicModule {
|
|||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
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();
|
||||
|
||||
spawn(async move {
|
||||
script.run(move |(out, _)| match out {
|
||||
script.run(None, move |out, _| match out {
|
||||
OutputStream::Stdout(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 gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{ApplicationWindow, Button, Orientation};
|
||||
use gtk::{ApplicationWindow, Orientation};
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -133,7 +133,7 @@ impl Popup {
|
|||
}
|
||||
|
||||
/// Shows the popup
|
||||
pub fn show(&self, geometry: ButtonGeometry) {
|
||||
pub fn show(&self, geometry: WidgetGeometry) {
|
||||
self.window.show();
|
||||
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
|
||||
/// (depending on orientation).
|
||||
fn set_pos(&self, geometry: ButtonGeometry) {
|
||||
fn set_pos(&self, geometry: WidgetGeometry) {
|
||||
let orientation = self.pos.get_orientation();
|
||||
|
||||
let mon_workarea = self.monitor.workarea();
|
||||
|
@ -190,14 +190,17 @@ impl Popup {
|
|||
|
||||
/// Gets the absolute X position of the button
|
||||
/// and its width / height (depending on orientation).
|
||||
pub fn button_pos(button: &Button, orientation: Orientation) -> ButtonGeometry {
|
||||
let button_size = if orientation == Orientation::Horizontal {
|
||||
button.allocation().width()
|
||||
pub fn widget_geometry<W>(widget: &W, orientation: Orientation) -> WidgetGeometry
|
||||
where
|
||||
W: IsA<gtk::Widget>,
|
||||
{
|
||||
let widget_size = if orientation == Orientation::Horizontal {
|
||||
widget.allocation().width()
|
||||
} 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 {
|
||||
top_level.allocation().width()
|
||||
|
@ -205,26 +208,26 @@ impl Popup {
|
|||
top_level.allocation().height()
|
||||
};
|
||||
|
||||
let (button_x, button_y) = button
|
||||
let (widget_x, widget_y) = widget
|
||||
.translate_coordinates(&top_level, 0, 0)
|
||||
.unwrap_or((0, 0));
|
||||
|
||||
let button_pos = if orientation == Orientation::Horizontal {
|
||||
button_x
|
||||
let widget_pos = if orientation == Orientation::Horizontal {
|
||||
widget_x
|
||||
} else {
|
||||
button_y
|
||||
widget_y
|
||||
};
|
||||
|
||||
ButtonGeometry {
|
||||
position: button_pos,
|
||||
size: button_size,
|
||||
WidgetGeometry {
|
||||
position: widget_pos,
|
||||
size: widget_size,
|
||||
bar_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ButtonGeometry {
|
||||
pub struct WidgetGeometry {
|
||||
position: i32,
|
||||
size: i32,
|
||||
bar_size: i32,
|
||||
|
|
|
@ -180,20 +180,22 @@ impl 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
|
||||
F: Fn((OutputStream, bool)),
|
||||
F: Fn(OutputStream, bool),
|
||||
{
|
||||
loop {
|
||||
match self.mode {
|
||||
ScriptMode::Poll => match self.get_output().await {
|
||||
Ok(output) => callback(output),
|
||||
ScriptMode::Poll => match self.get_output(args).await {
|
||||
Ok(output) => callback(output.0, output.1),
|
||||
Err(err) => error!("{err:?}"),
|
||||
},
|
||||
ScriptMode::Watch => match self.spawn().await {
|
||||
Ok(mut rx) => {
|
||||
while let Some(msg) = rx.recv().await {
|
||||
callback((msg, true));
|
||||
callback(msg, true);
|
||||
}
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
|
@ -210,9 +212,15 @@ impl Script {
|
|||
/// the `stdout` is returned.
|
||||
/// Otherwise, an `Err` variant
|
||||
/// 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")
|
||||
.args(["-c", &self.cmd])
|
||||
.args(&args_list)
|
||||
.output()
|
||||
.await
|
||||
.wrap_err("Failed to get script output")?;
|
||||
|
|
Loading…
Add table
Reference in a new issue