diff --git a/.github/scripts/ubuntu_setup.sh b/.github/scripts/ubuntu_setup.sh
index ea8f1cc..db9ca76 100755
--- a/.github/scripts/ubuntu_setup.sh
+++ b/.github/scripts/ubuntu_setup.sh
@@ -17,4 +17,5 @@ $SUDO apt-get update && $SUDO apt-get install --assume-yes \
libssl-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libgtk-3-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
libgtk-layer-shell-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
- libpulse-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH}
+ libpulse-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH} \
+ libluajit-5.1-dev${CROSS_DEB_ARCH:+:$CROSS_DEB_ARCH}
diff --git a/Cargo.lock b/Cargo.lock
index e32a942..1ef4674 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -334,6 +334,16 @@ dependencies = [
"log",
]
+[[package]]
+name = "bstr"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
[[package]]
name = "bumpalo"
version = "3.12.0"
@@ -354,9 +364,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cairo-rs"
-version = "0.18.3"
+version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f33613627f0dea6a731b0605101fad59ba4f193a52c96c4687728d822605a8a1"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
dependencies = [
"bitflags 2.4.0",
"cairo-sys-rs",
@@ -1584,6 +1594,7 @@ checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
name = "ironbar"
version = "0.15.0-pre"
dependencies = [
+ "cairo-rs",
"cfg-if",
"chrono",
"clap",
@@ -1597,6 +1608,8 @@ dependencies = [
"hyprland",
"indexmap",
"libpulse-binding",
+ "lua-src",
+ "mlua",
"mpd-utils",
"mpris",
"nix 0.27.1",
@@ -1761,6 +1774,15 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "lua-src"
+version = "546.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2da0daa7eee611a4c30c8f5ee31af55266e26e573971ba9336d2993e2da129b2"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "matchers"
version = "0.1.0"
@@ -1836,6 +1858,30 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[package]]
+name = "mlua"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "868d02cb5eb97761bbf6bd6922c1c7a88b8ea252bbf43bd8350a0bf8497a1fc0"
+dependencies = [
+ "bstr",
+ "mlua-sys",
+ "num-traits",
+ "once_cell",
+ "rustc-hash",
+]
+
+[[package]]
+name = "mlua-sys"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2847b42764435201d8cbee1f517edb79c4cca4181877b90047587c89e1b7bce4"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "pkg-config",
+]
+
[[package]]
name = "mpd-utils"
version = "0.2.1"
@@ -2549,6 +2595,12 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b"
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
[[package]]
name = "rustc_version"
version = "0.4.0"
diff --git a/Cargo.toml b/Cargo.toml
index ea98742..7ce8ae5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
[features]
default = [
"cli",
+ "cairo",
"clipboard",
"clock",
"config+all",
@@ -45,6 +46,8 @@ http = ["dep:reqwest"]
"config+corn" = ["universal-config/corn"]
"config+ron" = ["universal-config/ron"]
+cairo = ["lua-src", "mlua", "cairo-rs"]
+
clipboard = ["nix"]
clock = ["chrono"]
@@ -116,6 +119,11 @@ serde_json = { version = "1.0.114", optional = true }
# http
reqwest = { version = "0.12.3", default_features = false, features = ["default-tls", "http2"], optional = true }
+# cairo
+lua-src = { version = "546.0.2", optional = true }
+mlua = { version = "0.9.6", optional = true, features = ["luajit"] }
+cairo-rs = { version = "0.18.5", optional = true, features = ["png"] }
+
# clipboard
nix = { version = "0.27.1", optional = true, features = ["event"] }
diff --git a/docs/Compiling.md b/docs/Compiling.md
index f7eeb93..b16b3fb 100644
--- a/docs/Compiling.md
+++ b/docs/Compiling.md
@@ -49,8 +49,8 @@ dnf install libpulseaudio-devel
By default, all features are enabled for convenience. This can result in a significant compile time.
If you know you are not going to need all the features, you can compile with only the features you need.
-As of `v0.10.0`, compiling with no features is about 33% faster.
-On a 3800X, it takes about 60 seconds for no features and 90 seconds for all.
+As of `v0.15.0`, compiling with no features is about 50% faster.
+On a 3800X, it takes about 45 seconds for no features and 90 seconds for all.
This difference is expected to increase as the bar develops.
Features containing a `+` can be stacked, for example `config+json` and `config+yaml` could both be enabled.
@@ -77,6 +77,7 @@ cargo build --release --no-default-features \
| config+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). |
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
| **Modules** | |
+| cairo | Enables the `cairo` module |
| clipboard | Enables the `clipboard` module. |
| clock | Enables the `clock` module. |
| focused | Enables the `focused` module. |
diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md
index 48bdf62..471971c 100644
--- a/docs/_Sidebar.md
+++ b/docs/_Sidebar.md
@@ -24,6 +24,7 @@
# Modules
+- [Cairo](cairo)
- [Clipboard](clipboard)
- [Clock](clock)
- [Custom](custom)
diff --git a/docs/modules/Cairo.md b/docs/modules/Cairo.md
new file mode 100644
index 0000000..ff296f3
--- /dev/null
+++ b/docs/modules/Cairo.md
@@ -0,0 +1,215 @@
+Allows you to render custom content using the Lua and the Cairo drawing library.
+This is an advanced feature which provides a powerful escape hatch, allowing you to fetch data and render anything
+using an embedded scripting environment.
+
+Scripts are automatically hot-reloaded.
+
+> [!NOTE]
+> The Lua engine uses LuaJIT 5.1, and requires the use of a library called `lgi`.
+> Ensure you have the correct lua-lgi package installed.
+
+![Circle clock](https://f.jstanger.dev/github/ironbar/cairo-clock.png)
+
+## Configuration
+
+> Type: `cairo`
+
+| Name | Type | Default | Description |
+|--------------------|-----------|---------|----------------------------------------------------|
+| `path` | `string` | `null` | The path to the Lua script to load. |
+| `frequency` | `float` | `200` | The number of milliseconds between each draw call. |
+| `width` | `integer` | `42` | The canvas width in pixels. |
+| `height` | `integer` | `42` | The canvas height in pixels. |
+
+
+JSON
+
+```json
+{
+ "center": [
+ {
+ "type": "cairo",
+ "path": ".config/ironbar/clock.lua",
+ "frequency": 100,
+ "width": 300,
+ "height": 300
+ }
+ ]
+}
+
+```
+
+
+
+
+TOML
+
+```toml
+[[center]]
+type = "cairo"
+path = ".config/ironbar/clock.lua"
+frequency = 100
+width = 300
+height = 300
+```
+
+
+
+YAML
+
+```yaml
+center:
+- type: cairo
+ path: .config/ironbar/clock.lua
+ frequency: 100
+ width: 300
+ height: 300
+```
+
+
+
+
+Corn
+
+```corn
+let {
+ $config_dir = ".config/ironbar"
+ $cairo = {
+ type = "cairo"
+ path = "$config_dir/clock.lua"
+ frequency = 100
+ width = 300
+ height = 300
+ }
+} in {
+ center = [ $cairo ]
+}
+```
+
+
+
+### Script
+
+Every script must contain a function called `draw`.
+This takes a single parameter, which is the Cairo context.
+
+Outside of this, you can do whatever you like.
+The full lua `stdlib` is available, and you can load in additional system packages as desired.
+
+The most basic example, which draws a red square, can be seen below:
+
+```lua
+function draw(cr)
+ cr:set_source_rgb(1.0, 0.0, 0.0)
+ cr:paint()
+end
+```
+
+A longer example, used to create the clock in the image at the top of the page, is shown below:
+
+
+Circle clock
+
+```lua
+function get_ms()
+ local ms = tostring(io.popen('date +%s%3N'):read('a')):sub(-4, 9999)
+ return tonumber(ms) / 1000
+end
+
+function draw(cr)
+ local center_x = 150
+ local center_y = 150
+ local radius = 130
+
+ local date_table = os.date("*t")
+
+ local hours = date_table["hour"]
+ local minutes = date_table["min"]
+ local seconds = date_table["sec"]
+ local ms = get_ms()
+
+
+ local label_seconds = seconds
+ seconds = seconds + ms
+
+ local hours_str = tostring(hours)
+ if string.len(hours_str) == 1 then
+ hours_str = "0" .. hours_str
+ end
+
+ local minutes_str = tostring(minutes)
+ if string.len(minutes_str) == 1 then
+ minutes_str = "0" .. minutes_str
+ end
+
+ local seconds_str = tostring(label_seconds)
+ if string.len(seconds_str) == 1 then
+ seconds_str = "0" .. seconds_str
+ end
+
+ local font_size = radius / 5.5
+
+ cr:set_source_rgb(1.0, 1.0, 1.0)
+
+ cr:move_to(center_x - font_size * 2.5 + 10, center_y + font_size / 2.5)
+ cr:set_font_size(font_size)
+ cr:show_text(hours_str .. ':' .. minutes_str .. ':' .. seconds_str)
+ cr:stroke()
+
+ if hours > 12 then
+ hours = hours - 12
+ end
+
+ local line_width = radius / 8
+ local start_angle = -math.pi / 2
+
+ local end_angle = start_angle + ((hours + minutes / 60 + seconds / 3600) / 12) * 2 * math.pi
+ cr:set_line_width(line_width)
+ cr:arc(center_x, center_y, radius, start_angle, end_angle)
+ cr:stroke()
+
+ end_angle = start_angle + ((minutes + seconds / 60) / 60) * 2 * math.pi
+ cr:set_line_width(line_width)
+ cr:arc(center_x, center_y, radius * 0.8, start_angle, end_angle)
+ cr:stroke()
+
+ if seconds == 0 then
+ seconds = 60
+ end
+
+ end_angle = start_angle + (seconds / 60) * 2 * math.pi
+ cr:set_line_width(line_width)
+ cr:arc(center_x, center_y, radius * 0.6, start_angle, end_angle)
+ cr:stroke()
+
+ return 0
+end
+```
+
+
+
+> [!TIP]
+> The C documentation for the Cairo context interface can be found [here](https://www.cairographics.org/manual/cairo-cairo-t.html).
+> The Lua interface provides a slightly friendlier API which restructures things slightly.
+> The `cairo_` prefix is dropped, and the `cairo_t *cr` parameters are replaced with a namespaced call.
+> For example, `cairo_paint (cairo_t *cr)` becomes `cr:paint()`
+
+> [!TIP]
+> Ironbar's Cairo module has similar functionality to the popular Conky program.
+> You can often re-use scripts with little work.
+
+### Initialization
+
+You can optionally create an `init.lua` file in your config directory.
+Any code in here will be executed once, on bar startup.
+
+As variables and functions are global by default in Lua,
+this provides a mechanism for sharing code between multiple modules.
+
+## Styling
+
+| Selector | Description |
+|----------|-------------------------|
+| `.cairo` | Cairo widget container. |
+
+For more information on styling, please see the [styling guide](styling-guide).
\ No newline at end of file
diff --git a/flake.nix b/flake.nix
index f8ca6e2..25655d7 100644
--- a/flake.nix
+++ b/flake.nix
@@ -106,9 +106,11 @@
program = "${pkgs.ironbar}/bin/ironbar";
};
});
+
devShells = genSystems (system: let
pkgs = pkgsFor system;
rust = mkRustToolchain pkgs;
+
in {
default = pkgs.mkShell {
packages = with pkgs; [
@@ -128,11 +130,15 @@
gsettings-desktop-schemas
libxkbcommon
libpulseaudio
+ luajit
+ luajitPackages.lgi
];
RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
};
+
});
+
homeManagerModules.default = {
config,
lib,
diff --git a/lua/draw.lua b/lua/draw.lua
new file mode 100644
index 0000000..5bc2e5e
--- /dev/null
+++ b/lua/draw.lua
@@ -0,0 +1,4 @@
+function(id, ptr)
+ local cr = __lgi_core.record.new(cairo.Context, ptr)
+ _G['__draw_' .. id](cr)
+end
\ No newline at end of file
diff --git a/lua/init.lua b/lua/init.lua
new file mode 100644
index 0000000..a4baa39
--- /dev/null
+++ b/lua/init.lua
@@ -0,0 +1,4 @@
+local lgi = require('lgi')
+cairo = lgi.cairo
+
+__lgi_core = require('lgi.core')
diff --git a/nix/default.nix b/nix/default.nix
index 142bc6f..f013470 100644
--- a/nix/default.nix
+++ b/nix/default.nix
@@ -14,6 +14,8 @@
libxkbcommon,
libpulseaudio,
openssl,
+ luajit,
+ luajitPackages,
pkg-config,
hicolor-icon-theme,
rustPlatform,
@@ -30,11 +32,28 @@
name = "ironbar";
path = lib.cleanSource ../.;
};
- nativeBuildInputs = [pkg-config wrapGAppsHook gobject-introspection];
- buildInputs = [gtk3 gdk-pixbuf glib gtk-layer-shell glib-networking shared-mime-info gnome.adwaita-icon-theme hicolor-icon-theme gsettings-desktop-schemas libxkbcommon libpulseaudio openssl];
- propagatedBuildInputs = [
+ nativeBuildInputs = [ pkg-config wrapGAppsHook gobject-introspection ];
+
+ buildInputs = [
gtk3
+ gdk-pixbuf
+ glib
+ gtk-layer-shell
+ glib-networking
+ shared-mime-info
+ gnome.adwaita-icon-theme
+ hicolor-icon-theme
+ gsettings-desktop-schemas
+ libxkbcommon
+ libpulseaudio
+ openssl
+ luajit
];
+
+ propagatedBuildInputs = [ gtk3 ];
+
+ lgi = luajitPackages.lgi;
+
preFixup = ''
gappsWrapperArgs+=(
# Thumbnailers
@@ -45,6 +64,10 @@
# gtk-launch
--suffix PATH : "${lib.makeBinPath [ gtk3 ]}"
+
+ # cairo
+ --prefix LUA_PATH : "./?.lua;${lgi}/share/lua/5.1/?.lua;${lgi}/share/lua/5.1/?/init.lua;${luajit}/share/lua/5.1/\?.lua;${luajit}/share/lua/5.1/?/init.lua"
+ --prefix LUA_CPATH : "./?.so;${lgi}/lib/lua/5.1/?.so;${luajit}/lib/lua/5.1/?.so;${luajit}/lib/lua/5.1/loadall.so"
)
'';
passthru = {
diff --git a/shell.nix b/shell.nix
index 66f6b9d..b15e8e9 100644
--- a/shell.nix
+++ b/shell.nix
@@ -10,6 +10,8 @@ pkgs.mkShell {
gcc
openssl
libpulseaudio
+ luajit
+ luajitPackages.lgi
];
nativeBuildInputs = with pkgs; [
diff --git a/src/clients/lua.rs b/src/clients/lua.rs
new file mode 100644
index 0000000..04f8a58
--- /dev/null
+++ b/src/clients/lua.rs
@@ -0,0 +1,41 @@
+use mlua::Lua;
+use std::ops::Deref;
+use std::path::Path;
+use tracing::{debug, error};
+
+/// Wrapper around Lua instance
+/// to create a singleton and handle initialization.
+#[derive(Debug)]
+pub struct LuaEngine {
+ lua: Lua,
+}
+
+impl LuaEngine {
+ pub fn new(config_dir: &Path) -> Self {
+ let lua = unsafe { Lua::unsafe_new() };
+
+ let user_init = config_dir.join("init.lua");
+ if user_init.exists() {
+ debug!("loading user init script");
+
+ if let Err(err) = lua.load(user_init).exec() {
+ error!("{err:?}");
+ }
+ }
+
+ debug!("loading internal init script");
+ if let Err(err) = lua.load(include_str!("../../lua/init.lua")).exec() {
+ error!("{err:?}");
+ }
+
+ Self { lua }
+ }
+}
+
+impl Deref for LuaEngine {
+ type Target = Lua;
+
+ fn deref(&self) -> &Self::Target {
+ &self.lua
+ }
+}
diff --git a/src/clients/mod.rs b/src/clients/mod.rs
index f289c2c..b44f0df 100644
--- a/src/clients/mod.rs
+++ b/src/clients/mod.rs
@@ -1,11 +1,15 @@
use crate::{await_sync, Ironbar};
use color_eyre::Result;
+use std::path::Path;
+use std::rc::Rc;
use std::sync::Arc;
#[cfg(feature = "clipboard")]
pub mod clipboard;
#[cfg(feature = "workspaces")]
pub mod compositor;
+#[cfg(feature = "cairo")]
+pub mod lua;
#[cfg(feature = "music")]
pub mod music;
#[cfg(feature = "notifications")]
@@ -27,6 +31,8 @@ pub struct Clients {
workspaces: Option>,
#[cfg(feature = "clipboard")]
clipboard: Option>,
+ #[cfg(feature = "cairo")]
+ lua: Option>,
#[cfg(feature = "music")]
music: std::collections::HashMap>,
#[cfg(feature = "notifications")]
@@ -75,6 +81,13 @@ impl Clients {
Ok(client)
}
+ #[cfg(feature = "cairo")]
+ pub fn lua(&mut self, config_dir: &Path) -> Rc {
+ self.lua
+ .get_or_insert_with(|| Rc::new(lua::LuaEngine::new(config_dir)))
+ .clone()
+ }
+
#[cfg(feature = "music")]
pub fn music(&mut self, client_type: music::ClientType) -> Arc {
self.music
diff --git a/src/config/mod.rs b/src/config/mod.rs
index e57fac8..44ee8db 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -2,6 +2,8 @@ mod common;
mod r#impl;
mod truncate;
+#[cfg(feature = "cairo")]
+use crate::modules::cairo::CairoModule;
#[cfg(feature = "clipboard")]
use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")]
@@ -40,6 +42,8 @@ pub use self::truncate::TruncateMode;
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig {
+ #[cfg(feature = "cairo")]
+ Cairo(Box),
#[cfg(feature = "clipboard")]
Clipboard(Box),
#[cfg(feature = "clock")]
@@ -81,6 +85,8 @@ impl ModuleConfig {
}
match self {
+ #[cfg(feature = "cairo")]
+ Self::Cairo(module) => create!(module),
#[cfg(feature = "clipboard")]
Self::Clipboard(module) => create!(module),
#[cfg(feature = "clock")]
diff --git a/src/main.rs b/src/main.rs
index 7a9f116..0df08ea 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -96,14 +96,18 @@ pub struct Ironbar {
bars: Rc>>,
clients: Rc>,
config: Rc>,
+ config_dir: PathBuf,
}
impl Ironbar {
fn new() -> Self {
+ let (config, config_dir) = load_config();
+
Self {
bars: Rc::new(RefCell::new(vec![])),
clients: Rc::new(RefCell::new(Clients::new())),
- config: Rc::new(RefCell::new(load_config())),
+ config: Rc::new(RefCell::new(config)),
+ config_dir,
}
}
@@ -260,7 +264,7 @@ impl Ironbar {
/// Note this does *not* reload bars, which must be performed separately.
#[cfg(feature = "ipc")]
fn reload_config(&self) {
- self.config.replace(load_config());
+ self.config.replace(load_config().0);
}
}
@@ -270,20 +274,37 @@ fn start_ironbar() {
}
/// Loads the config file from disk.
-fn load_config() -> Config {
- let mut config = env::var("IRONBAR_CONFIG")
- .map_or_else(
- |_| ConfigLoader::new("ironbar").find_and_load(),
- ConfigLoader::load,
- )
- .unwrap_or_else(|err| {
- error!("Failed to load config: {}", err);
- warn!("Falling back to the default config");
- info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
- info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
+fn load_config() -> (Config, PathBuf) {
+ let config_path = env::var("IRONBAR_CONFIG");
- Config::default()
- });
+ let (config, directory) = if let Ok(config_path) = config_path {
+ let path = PathBuf::from(config_path);
+ (
+ ConfigLoader::load(&path),
+ path.parent()
+ .map(PathBuf::from)
+ .ok_or_else(|| Report::msg("Specified path has no parent")),
+ )
+ } else {
+ let config_loader = ConfigLoader::new("ironbar");
+ (
+ config_loader.find_and_load(),
+ config_loader.config_dir().map_err(Report::new),
+ )
+ };
+
+ let mut config = config.unwrap_or_else(|err| {
+ error!("Failed to load config: {}", err);
+ warn!("Falling back to the default config");
+ info!("If this is your first time using Ironbar, you should create a config in ~/.config/ironbar/");
+ info!("More info here: https://github.com/JakeStanger/ironbar/wiki/configuration-guide");
+
+ Config::default()
+ });
+
+ let directory = directory
+ .and_then(|dir| dir.canonicalize().map_err(Report::new))
+ .unwrap_or_else(|_| env::current_dir().expect("to have current working directory"));
debug!("Loaded config file");
@@ -297,7 +318,7 @@ fn load_config() -> Config {
}
}
- config
+ (config, directory)
}
/// Gets the GDK `Display` instance.
diff --git a/src/modules/cairo.rs b/src/modules/cairo.rs
new file mode 100644
index 0000000..9e21aec
--- /dev/null
+++ b/src/modules/cairo.rs
@@ -0,0 +1,198 @@
+use crate::config::CommonConfig;
+use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
+use crate::{glib_recv, module_impl, spawn, try_send};
+use cairo::{Format, ImageSurface};
+use glib::translate::IntoGlibPtr;
+use glib::Propagation;
+use gtk::prelude::*;
+use gtk::DrawingArea;
+use mlua::{Error, Function, LightUserData};
+use notify::event::ModifyKind;
+use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Watcher};
+use serde::Deserialize;
+use std::fs;
+use std::path::PathBuf;
+use std::time::Duration;
+use tokio::sync::mpsc::Receiver;
+use tokio::time::sleep;
+use tracing::{debug, error};
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct CairoModule {
+ path: PathBuf,
+
+ #[serde(default = "default_frequency")]
+ frequency: u64,
+
+ #[serde(default = "default_size")]
+ width: u32,
+ #[serde(default = "default_size")]
+ height: u32,
+
+ #[serde(flatten)]
+ pub common: Option,
+}
+
+const fn default_size() -> u32 {
+ 42
+}
+
+const fn default_frequency() -> u64 {
+ 200
+}
+
+impl Module for CairoModule {
+ type SendMessage = ();
+ type ReceiveMessage = ();
+
+ module_impl!("cairo");
+
+ fn spawn_controller(
+ &self,
+ _info: &ModuleInfo,
+ context: &WidgetContext,
+ _rx: Receiver,
+ ) -> color_eyre::Result<()>
+ where
+ >::SendMessage: Clone,
+ {
+ let path = self.path.to_path_buf();
+
+ let tx = context.tx.clone();
+ spawn(async move {
+ let parent = path.parent().expect("to have parent path");
+
+ let mut watcher = recommended_watcher({
+ let path = path.clone();
+ move |res: notify::Result| match res {
+ Ok(event) if matches!(event.kind, EventKind::Modify(ModifyKind::Data(_))) => {
+ debug!("{event:?}");
+
+ if event.paths.first().is_some_and(|p| p == &path) {
+ try_send!(tx, ModuleUpdateEvent::Update(()));
+ }
+ }
+ Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
+ _ => {}
+ }
+ })
+ .expect("Failed to create lua file watcher");
+
+ watcher
+ .watch(parent, RecursiveMode::NonRecursive)
+ .expect("Failed to start lua file watcher");
+
+ // avoid watcher from dropping
+ loop {
+ sleep(Duration::from_secs(1)).await;
+ }
+ });
+
+ // Lua needs to run synchronously with the GTK updates,
+ // so the controller does not handle the script engine.
+
+ Ok(())
+ }
+
+ fn into_widget(
+ self,
+ context: WidgetContext,
+ info: &ModuleInfo,
+ ) -> color_eyre::Result>
+ where
+ >::SendMessage: Clone,
+ {
+ let id = context.id.to_string();
+
+ let container = gtk::Box::new(info.bar_position.orientation(), 0);
+
+ let surface = ImageSurface::create(Format::ARgb32, self.width as i32, self.height as i32)?;
+
+ let area = DrawingArea::new();
+
+ let lua = context
+ .ironbar
+ .clients
+ .borrow_mut()
+ .lua(&context.ironbar.config_dir);
+
+ // this feels kinda dirty,
+ // but it keeps draw functions separate in the global scope
+ let script = fs::read_to_string(&self.path)?
+ .replace("function draw", format!("function __draw_{id}").as_str());
+ lua.load(&script).exec()?;
+
+ {
+ let lua = lua.clone();
+ let id = id.clone();
+
+ let path = self.path.clone();
+
+ area.connect_draw(move |_, cr| {
+ let function: Function = lua
+ .load(include_str!("../../lua/draw.lua"))
+ .eval()
+ .expect("to be valid");
+
+ if let Err(err) = cr.set_source_surface(&surface, 0.0, 0.0) {
+ error!("{err}");
+ return Propagation::Stop;
+ }
+
+ let ptr = unsafe { cr.clone().into_glib_ptr().cast() };
+
+ // mlua needs a valid return type, even if we don't return anything
+
+ if let Err(err) =
+ function.call::<_, Option>((id.as_str(), LightUserData(ptr)))
+ {
+ match err {
+ Error::RuntimeError(message) => {
+ let message = message.split_once("]:").expect("to exist").1;
+ error!("[lua runtime error] {}:{message}", path.display())
+ }
+ _ => error!("{err}"),
+ }
+
+ return Propagation::Stop;
+ }
+
+ Propagation::Proceed
+ });
+ }
+
+ area.set_size_request(self.width as i32, self.height as i32);
+ container.add(&area);
+
+ glib::spawn_future_local(async move {
+ loop {
+ area.queue_draw();
+ glib::timeout_future(Duration::from_millis(self.frequency)).await;
+ }
+ });
+
+ glib_recv!(context.subscribe(), _ev => {
+ let res = fs::read_to_string(&self.path)
+ .map(|s| s.replace("function draw", format!("function __draw_{id}").as_str()));
+
+ match res {
+ Ok(script) => {
+ match lua.load(&script).exec() {
+ Ok(_) => {},
+ Err(Error::SyntaxError { message, ..}) => {
+ let message = message.split_once("]:").expect("to exist").1;
+ error!("[lua syntax error] {}:{message}", self.path.display())
+ },
+ Err(err) => error!("lua error: {err:?}")
+ }
+ },
+ Err(err) => error!("{err:?}")
+ }
+ });
+
+ Ok(ModuleParts {
+ widget: container,
+ popup: None,
+ })
+ }
+}
diff --git a/src/modules/mod.rs b/src/modules/mod.rs
index 0e7474f..ec3bb65 100644
--- a/src/modules/mod.rs
+++ b/src/modules/mod.rs
@@ -16,6 +16,8 @@ use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup;
use crate::{glib_recv_mpsc, send, Ironbar};
+#[cfg(feature = "cairo")]
+pub mod cairo;
#[cfg(feature = "clipboard")]
pub mod clipboard;
/// Displays the current date and time.