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