From c9e66d4664137c50aba4aecdc3a3ba43d3da11fe Mon Sep 17 00:00:00 2001 From: Jake Stanger Date: Mon, 28 Nov 2022 21:55:08 +0000 Subject: [PATCH] feat: common module options (`show_if`, `on_click`, `tooltip`) The first three of many options that are common to all modules. Resolves #36. Resolves partially #34. --- docs/Configuration guide.md | 26 ++- docs/Scripts.md | 91 +++++++++ docs/_Sidebar.md | 1 + src/bar.rs | 63 +++++- src/config.rs | 8 + src/modules/clock.rs | 6 +- src/modules/custom.rs | 13 +- src/modules/focused.rs | 4 + src/modules/launcher/mod.rs | 4 + src/modules/mpd.rs | 4 + src/modules/script.rs | 115 +++-------- src/modules/sysinfo.rs | 4 + src/modules/tray.rs | 6 +- src/modules/workspaces.rs | 4 + src/script.rs | 376 +++++++++++++++++++++++++++++++++--- 15 files changed, 600 insertions(+), 125 deletions(-) create mode 100644 docs/Scripts.md diff --git a/docs/Configuration guide.md b/docs/Configuration guide.md index 3b7b3dd..9d50132 100644 --- a/docs/Configuration guide.md +++ b/docs/Configuration guide.md @@ -1,6 +1,12 @@ By default, you get a single bar at the bottom of all your screens. To change that, you'll unsurprisingly need a config file. +This page details putting together the skeleton for your config to get you to a stage where you can start configuring modules. +It may look long and overwhelming, but that is just because the bar supports a lot of scenarios! + +If you want to see some ready-to-go config files check the [examples folder](https://github.com/JakeStanger/ironbar/tree/master/examples) +and the example pages in the sidebar. + ## 1. Create config file The config file lives inside the `ironbar` directory in your XDG_CONFIG_DIR, which is usually `~/.config/ironbar`. @@ -253,8 +259,11 @@ monitors: Once you have the basic config structure set up, it's time to actually configure your bar(s). -The following table describes each of the top-level bar config options. -For details on available modules and each of their config options, check the sidebar. +Check [here](config) for an example config file for a fully configured bar in each format. + +### 3.1 Top-level options + +The following table lists each of the top-level bar config options: | Name | Type | Default | Description | |-------------------|----------------------------------------|----------|-----------------------------------------------------------------------------------------| @@ -265,4 +274,15 @@ For details on available modules and each of their config options, check the sid | `center` | `Module[]` | `[]` | Array of center modules. | | `end` | `Module[]` | `[]` | Array of right or bottom modules. | -Check [here](config) for an example config file for a fully configured bar in each format. \ No newline at end of file +### 3.2 Module-level options + +The following table lists each of the module-level options that are present on **all** modules. +For details on available modules and each of their config options, check the sidebar. + +For information on the `Script` type, see [here](script). + +| Name | Type | Default | Description | +|------------|--------------------|---------|--------------------------------------------------------------------------------------------------------------------| +| `show_if` | `Script [polling]` | `null` | Polls the script to check its exit code. If exit code is zero, the module is shown. For other codes, it is hidden. | +| `on_click` | `Script [polling]` | `null` | Runs the script when the module is clicked. | +| `tooltip` | `string` | `null` | Shows this text on hover. | diff --git a/docs/Scripts.md b/docs/Scripts.md new file mode 100644 index 0000000..b5fde4a --- /dev/null +++ b/docs/Scripts.md @@ -0,0 +1,91 @@ +There are various places inside the configuration (other than the `script` module) +that allow script input to dynamically set values. + +Scripts are passed to `sh -c`. + +Two types of scripts exist: polling and watching: + +- Polling scripts will run and wait for exit. + Normally they will repeat this at an interval, hence the name, although in some cases they may only run on a user + event. + If the script exited code 0, the `stdout` will be used. Otherwise, `stderr` will be printed to the log. +- Watching scripts start a long-running process. Every time the process writes to `stdout`, the last line is captured + and used. + +One should prefer to use watch-mode where possible, as it removes the overhead of regularly spawning processes. +That said, there are some cases which only support polling. These are indicated by `Script [polling]` as the option +type. + +## Writing script configs + +There are two available config formats for scripts, shorthand as a string, or longhand as an object. +Shorthand can be used in all cases, but there are some cases (such as embedding scripts inside strings) where longhand +cannot be used. + +In both formats, `mode` is one of `poll` or `watch` and `interval` is the number of milliseconds to wait between +spawning the script. + +Both `mode` and `interval` are optional and can be excluded to fall back to their defaults of `poll` and `5000` +respectively. + +### Shorthand (string) + +Shorthand scripts should be written in the format: + +``` +mode:interval:script +``` + +For example: + +``` +poll:5000:uptime -p | cut -d ' ' -f2- +``` + +### Longhand (object) + +An object consisting of the `cmd` key and optionally the `mode` and/or `interval` keys. + +
+JSON + +```json +{ + "mode": "poll", + "interval": 5000, + "cmd": "uptime -p | cut -d ' ' -f2-" +} +``` +
+ +
+YAML + +```yaml +mode: poll +interval: 5000 +cmd: "uptime -p | cut -d ' ' -f2-" +``` +
+ +
+YAML + +```toml +mode = "poll" +interval = 5000 +cmd = "uptime -p | cut -d ' ' -f2-" +``` +
+ +
+Corn + +```corn +{ + mode = "poll" + interval = 5000 + cmd = "uptime -p | cut -d ' ' -f2-" +} +``` +
\ No newline at end of file diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index d274cec..90cbc0d 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -1,6 +1,7 @@ # Guides - [Configuration guide](configuration-guide) + - [Scripts](scripts) - [Styling guide](styling-guide) # Examples diff --git a/src/bar.rs b/src/bar.rs index 0011f61..2ca6501 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -6,7 +6,8 @@ use crate::modules::mpd::{PlayerCommand, SongUpdate}; use crate::modules::workspaces::WorkspaceUpdate; use crate::modules::{Module, ModuleInfoBuilder, ModuleLocation, ModuleUpdateEvent, WidgetContext}; use crate::popup::Popup; -use crate::Config; +use crate::script::{OutputStream, Script}; +use crate::{await_sync, Config}; use chrono::{DateTime, Local}; use color_eyre::Result; use gtk::gdk::Monitor; @@ -16,8 +17,9 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; use stray::message::NotifierItemCommand; use stray::NotifierItemMessage; +use tokio::spawn; use tokio::sync::mpsc; -use tracing::{debug, info}; +use tracing::{debug, error, info, trace}; /// Creates a new window for a bar, /// sets it up and adds its widgets. @@ -81,7 +83,11 @@ pub fn create_bar( }); debug!("Showing bar"); - win.show_all(); + start.show(); + center.show(); + end.show(); + content.show(); + win.show(); Ok(()) } @@ -155,11 +161,60 @@ fn add_modules( controller_tx: ui_tx, }; + let common = $module.common.clone(); + let widget = $module.into_widget(context, &info)?; - content.add(&widget.widget); + let container = gtk::EventBox::new(); + container.add(&widget.widget); + + content.add(&container); widget.widget.set_widget_name(info.module_name); + if let Some(show_if) = common.show_if { + let script = Script::new_polling(show_if); + let container = container.clone(); + + let (tx, rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT); + + spawn(async move { + script + .run(|(_, success)| { + tx.send(success) + .expect("Failed to send widget visibility toggle message"); + }) + .await; + }); + + rx.attach(None, move |success| { + if success { + container.show_all() + } else { + container.hide() + }; + Continue(true) + }); + } else { + container.show_all(); + } + + if let Some(on_click) = common.on_click { + let script = Script::new_polling(on_click); + container.connect_button_press_event(move |_, _| { + trace!("Running on-click script"); + match await_sync(async { script.get_output().await }) { + Ok((OutputStream::Stderr(out), _)) => error!("{out}"), + Err(err) => error!("{err:?}"), + _ => {} + } + Inhibit(false) + }); + } + + if let Some(tooltip) = common.tooltip { + container.set_tooltip_text(Some(&tooltip)); + } + let has_popup = widget.popup.is_some(); if let Some(popup_content) = widget.popup { popup diff --git a/src/config.rs b/src/config.rs index c30ed9d..1d0114c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,7 @@ use crate::modules::script::ScriptModule; use crate::modules::sysinfo::SysInfoModule; use crate::modules::tray::TrayModule; use crate::modules::workspaces::WorkspacesModule; +use crate::script::ScriptInput; use color_eyre::eyre::{Context, ContextCompat}; use color_eyre::{eyre, Help, Report}; use dirs::config_dir; @@ -18,6 +19,13 @@ use std::path::{Path, PathBuf}; use std::{env, fs}; use tracing::instrument; +#[derive(Debug, Deserialize, Clone)] +pub struct CommonConfig { + pub show_if: Option, + pub on_click: Option, + pub tooltip: Option, +} + #[derive(Debug, Deserialize, Clone)] #[serde(tag = "type", rename_all = "kebab-case")] pub enum ModuleConfig { diff --git a/src/modules/clock.rs b/src/modules/clock.rs index c418573..5b59698 100644 --- a/src/modules/clock.rs +++ b/src/modules/clock.rs @@ -1,3 +1,4 @@ +use crate::config::CommonConfig; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::popup::Popup; use chrono::{DateTime, Local}; @@ -18,7 +19,10 @@ pub struct ClockModule { /// Detail on available tokens can be found here: /// #[serde(default = "default_format")] - pub(crate) format: String, + format: String, + + #[serde(flatten)] + pub common: CommonConfig, } fn default_format() -> String { diff --git a/src/modules/custom.rs b/src/modules/custom.rs index c1b5736..ca6997d 100644 --- a/src/modules/custom.rs +++ b/src/modules/custom.rs @@ -1,7 +1,8 @@ use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::popup::{ButtonGeometry, Popup}; -use crate::script::exec_command; +use crate::config::CommonConfig; use color_eyre::{Report, Result}; +use crate::script::Script; use gtk::prelude::*; use gtk::{Button, Label, Orientation}; use serde::Deserialize; @@ -17,6 +18,9 @@ pub struct CustomModule { bar: Vec, /// Widgets to add to the popup container popup: Option>, + + #[serde(flatten)] + pub common: CommonConfig, } /// Attempts to parse an `Orientation` from `String` @@ -164,8 +168,11 @@ impl Module for CustomModule { spawn(async move { while let Some(event) = rx.recv().await { if event.cmd.starts_with('!') { - debug!("executing command: '{}'", &event.cmd[1..]); - if let Err(err) = exec_command(&event.cmd[1..]) { + 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" { diff --git a/src/modules/focused.rs b/src/modules/focused.rs index ad9f46a..3ba477d 100644 --- a/src/modules/focused.rs +++ b/src/modules/focused.rs @@ -1,4 +1,5 @@ use crate::clients::wayland::{self, ToplevelChange}; +use crate::config::CommonConfig; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::{await_sync, icon}; use color_eyre::Result; @@ -23,6 +24,9 @@ pub struct FocusedModule { icon_size: i32, /// GTK icon theme to use. icon_theme: Option, + + #[serde(flatten)] + pub common: CommonConfig, } const fn default_icon_size() -> i32 { diff --git a/src/modules/launcher/mod.rs b/src/modules/launcher/mod.rs index c5f24f5..a80b015 100644 --- a/src/modules/launcher/mod.rs +++ b/src/modules/launcher/mod.rs @@ -4,6 +4,7 @@ mod open_state; use self::item::{Item, ItemButton, Window}; use self::open_state::OpenState; use crate::clients::wayland::{self, ToplevelChange}; +use crate::config::CommonConfig; use crate::icon::find_desktop_file; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use color_eyre::{Help, Report}; @@ -33,6 +34,9 @@ pub struct LauncherModule { /// Name of the GTK icon theme to use. icon_theme: Option, + + #[serde(flatten)] + pub common: CommonConfig, } #[derive(Debug, Clone)] diff --git a/src/modules/mpd.rs b/src/modules/mpd.rs index 012533d..ce29ed6 100644 --- a/src/modules/mpd.rs +++ b/src/modules/mpd.rs @@ -1,4 +1,5 @@ use crate::clients::mpd::{get_client, get_duration, get_elapsed, MpdConnectionError}; +use crate::config::CommonConfig; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; use crate::popup::Popup; use color_eyre::Result; @@ -65,6 +66,9 @@ pub struct MpdModule { /// Path to root of music directory. #[serde(default = "default_music_dir")] music_dir: PathBuf, + + #[serde(flatten)] + pub common: CommonConfig, } fn default_socket() -> String { diff --git a/src/modules/script.rs b/src/modules/script.rs index 6af02c2..ce1181f 100644 --- a/src/modules/script.rs +++ b/src/modules/script.rs @@ -1,39 +1,32 @@ +use crate::config::CommonConfig; use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext}; -use crate::script::exec_command; +use crate::script::{OutputStream, Script, ScriptMode}; use color_eyre::{Help, Report, Result}; use gtk::prelude::*; use gtk::Label; use serde::Deserialize; -use std::process::Stdio; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; +use tokio::spawn; use tokio::sync::mpsc::{Receiver, Sender}; -use tokio::time::sleep; -use tokio::{select, spawn}; use tracing::error; -#[derive(Debug, Deserialize, Clone, Copy)] -#[serde(rename_all = "kebab-case")] -enum Mode { - Poll, - Watch, -} - #[derive(Debug, Deserialize, Clone)] pub struct ScriptModule { /// Path to script to execute. path: String, /// Script execution mode #[serde(default = "default_mode")] - mode: Mode, + mode: ScriptMode, /// Time in milliseconds between executions. #[serde(default = "default_interval")] interval: u64, + + #[serde(flatten)] + pub common: CommonConfig, } /// `Mode::Poll` -const fn default_mode() -> Mode { - Mode::Poll +const fn default_mode() -> ScriptMode { + ScriptMode::Poll } /// 5000ms @@ -41,6 +34,16 @@ const fn default_interval() -> u64 { 5000 } +impl From<&ScriptModule> for Script { + fn from(module: &ScriptModule) -> Self { + Self { + mode: module.mode, + cmd: module.path.clone(), + interval: module.interval, + } + } +} + impl Module