feat: new cairo module

Resolves #105

Co-authored-by: A-Cloud-Ninja <5809177+A-Cloud-Ninja@users.noreply.github.com>
This commit is contained in:
Jake Stanger 2024-03-10 12:51:23 +00:00
parent 7b089495ad
commit b0a05b7cda
No known key found for this signature in database
GPG Key ID: C51FC8F9CB0BEA61
13 changed files with 587 additions and 21 deletions

56
Cargo.lock generated
View File

@ -334,6 +334,16 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "bstr"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.12.0" version = "3.12.0"
@ -354,9 +364,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.18.3" version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f33613627f0dea6a731b0605101fad59ba4f193a52c96c4687728d822605a8a1" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
dependencies = [ dependencies = [
"bitflags 2.4.0", "bitflags 2.4.0",
"cairo-sys-rs", "cairo-sys-rs",
@ -1584,6 +1594,7 @@ checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
name = "ironbar" name = "ironbar"
version = "0.15.0-pre" version = "0.15.0-pre"
dependencies = [ dependencies = [
"cairo-rs",
"cfg-if", "cfg-if",
"chrono", "chrono",
"clap", "clap",
@ -1597,6 +1608,8 @@ dependencies = [
"hyprland", "hyprland",
"indexmap", "indexmap",
"libpulse-binding", "libpulse-binding",
"lua-src",
"mlua",
"mpd-utils", "mpd-utils",
"mpris", "mpris",
"nix 0.27.1", "nix 0.27.1",
@ -1761,6 +1774,15 @@ dependencies = [
"cfg-if", "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]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -1836,6 +1858,30 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "mpd-utils" name = "mpd-utils"
version = "0.2.1" version = "0.2.1"
@ -2549,6 +2595,12 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.0" version = "0.4.0"

View File

@ -11,6 +11,7 @@ keywords = ["gtk", "bar", "wayland", "wlroots", "gtk-layer-shell"]
[features] [features]
default = [ default = [
"cli", "cli",
"cairo",
"clipboard", "clipboard",
"clock", "clock",
"config+all", "config+all",
@ -45,6 +46,8 @@ http = ["dep:reqwest"]
"config+corn" = ["universal-config/corn"] "config+corn" = ["universal-config/corn"]
"config+ron" = ["universal-config/ron"] "config+ron" = ["universal-config/ron"]
cairo = ["lua-src", "mlua", "cairo-rs"]
clipboard = ["nix"] clipboard = ["nix"]
clock = ["chrono"] clock = ["chrono"]
@ -114,7 +117,12 @@ clap = { version = "4.5.4", optional = true, features = ["derive"] }
serde_json = { version = "1.0.114", optional = true } serde_json = { version = "1.0.114", optional = true }
# http # http
reqwest = { version = "0.12.3", default_features = false, features = ["default-tls", "http2"], default_features = false, features = ["default-tls", "http2"], optional = true } 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 # clipboard
nix = { version = "0.27.1", optional = true, features = ["event"] } nix = { version = "0.27.1", optional = true, features = ["event"] }

View File

@ -49,8 +49,8 @@ dnf install libpulseaudio-devel
By default, all features are enabled for convenience. This can result in a significant compile time. 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. 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. As of `v0.15.0`, compiling with no features is about 50% faster.
On a 3800X, it takes about 60 seconds for no features and 90 seconds for all. 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. 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. 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+corn | Enables configuration support for [Corn](https://github.com/jakestanger/corn). |
| config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). | | config+ron | Enables configuration support for [Ron](https://github.com/ron-rs/ron). |
| **Modules** | | | **Modules** | |
| cairo | Enables the `cairo` module |
| clipboard | Enables the `clipboard` module. | | clipboard | Enables the `clipboard` module. |
| clock | Enables the `clock` module. | | clock | Enables the `clock` module. |
| focused | Enables the `focused` module. | | focused | Enables the `focused` module. |

View File

@ -23,6 +23,7 @@
# Modules # Modules
- [Cairo](cairo)
- [Clipboard](clipboard) - [Clipboard](clipboard)
- [Clock](clock) - [Clock](clock)
- [Custom](custom) - [Custom](custom)

215
docs/modules/Cairo.md Normal file
View File

@ -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. |
<details>
<summary>JSON</summary>
```json
{
"center": [
{
"type": "cairo",
"path": ".config/ironbar/clock.lua",
"frequency": 100,
"width": 300,
"height": 300
}
]
}
```
</details>
<details>
<summary>TOML</summary>
```toml
[[center]]
type = "cairo"
path = ".config/ironbar/clock.lua"
frequency = 100
width = 300
height = 300
```
</details>
<details>
<summary>YAML</summary>
```yaml
center:
- type: cairo
path: .config/ironbar/clock.lua
frequency: 100
width: 300
height: 300
```
</details>
<details>
<summary>Corn</summary>
```corn
let {
$config_dir = ".config/ironbar"
$cairo = {
type = "cairo"
path = "$config_dir/clock.lua"
frequency = 100
width = 300
height = 300
}
} in {
center = [ $cairo ]
}
```
</details>
### 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:
<details>
<summary>Circle clock</summary>
```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
```
</details>
> [!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).

4
lua/draw.lua Normal file
View File

@ -0,0 +1,4 @@
function(id, ptr)
local cr = __lgi_core.record.new(cairo.Context, ptr)
_G['__draw_' .. id](cr)
end

4
lua/init.lua Normal file
View File

@ -0,0 +1,4 @@
local lgi = require('lgi')
cairo = lgi.cairo
__lgi_core = require('lgi.core')

41
src/clients/lua.rs Normal file
View File

@ -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
}
}

View File

@ -1,11 +1,15 @@
use crate::{await_sync, Ironbar}; use crate::{await_sync, Ironbar};
use color_eyre::Result; use color_eyre::Result;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub mod clipboard; pub mod clipboard;
#[cfg(feature = "workspaces")] #[cfg(feature = "workspaces")]
pub mod compositor; pub mod compositor;
#[cfg(feature = "cairo")]
pub mod lua;
#[cfg(feature = "music")] #[cfg(feature = "music")]
pub mod music; pub mod music;
#[cfg(feature = "notifications")] #[cfg(feature = "notifications")]
@ -27,6 +31,8 @@ pub struct Clients {
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>, workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
clipboard: Option<Arc<clipboard::Client>>, clipboard: Option<Arc<clipboard::Client>>,
#[cfg(feature = "cairo")]
lua: Option<Rc<lua::LuaEngine>>,
#[cfg(feature = "music")] #[cfg(feature = "music")]
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>, music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
#[cfg(feature = "notifications")] #[cfg(feature = "notifications")]
@ -75,6 +81,13 @@ impl Clients {
Ok(client) Ok(client)
} }
#[cfg(feature = "cairo")]
pub fn lua(&mut self, config_dir: &Path) -> Rc<lua::LuaEngine> {
self.lua
.get_or_insert_with(|| Rc::new(lua::LuaEngine::new(config_dir)))
.clone()
}
#[cfg(feature = "music")] #[cfg(feature = "music")]
pub fn music(&mut self, client_type: music::ClientType) -> Arc<dyn music::MusicClient> { pub fn music(&mut self, client_type: music::ClientType) -> Arc<dyn music::MusicClient> {
self.music self.music

View File

@ -2,6 +2,8 @@ mod common;
mod r#impl; mod r#impl;
mod truncate; mod truncate;
#[cfg(feature = "cairo")]
use crate::modules::cairo::CairoModule;
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
use crate::modules::clipboard::ClipboardModule; use crate::modules::clipboard::ClipboardModule;
#[cfg(feature = "clock")] #[cfg(feature = "clock")]
@ -40,6 +42,8 @@ pub use self::truncate::TruncateMode;
#[derive(Debug, Deserialize, Clone)] #[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum ModuleConfig { pub enum ModuleConfig {
#[cfg(feature = "cairo")]
Cairo(Box<CairoModule>),
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
Clipboard(Box<ClipboardModule>), Clipboard(Box<ClipboardModule>),
#[cfg(feature = "clock")] #[cfg(feature = "clock")]
@ -81,6 +85,8 @@ impl ModuleConfig {
} }
match self { match self {
#[cfg(feature = "cairo")]
Self::Cairo(module) => create!(module),
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
Self::Clipboard(module) => create!(module), Self::Clipboard(module) => create!(module),
#[cfg(feature = "clock")] #[cfg(feature = "clock")]

View File

@ -96,14 +96,18 @@ pub struct Ironbar {
bars: Rc<RefCell<Vec<Bar>>>, bars: Rc<RefCell<Vec<Bar>>>,
clients: Rc<RefCell<Clients>>, clients: Rc<RefCell<Clients>>,
config: Rc<RefCell<Config>>, config: Rc<RefCell<Config>>,
config_dir: PathBuf,
} }
impl Ironbar { impl Ironbar {
fn new() -> Self { fn new() -> Self {
let (config, config_dir) = load_config();
Self { Self {
bars: Rc::new(RefCell::new(vec![])), bars: Rc::new(RefCell::new(vec![])),
clients: Rc::new(RefCell::new(Clients::new())), 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. /// Note this does *not* reload bars, which must be performed separately.
#[cfg(feature = "ipc")] #[cfg(feature = "ipc")]
fn reload_config(&self) { 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. /// Loads the config file from disk.
fn load_config() -> Config { fn load_config() -> (Config, PathBuf) {
let mut config = env::var("IRONBAR_CONFIG") let config_path = 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");
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"); debug!("Loaded config file");
@ -297,7 +318,7 @@ fn load_config() -> Config {
} }
} }
config (config, directory)
} }
/// Gets the GDK `Display` instance. /// Gets the GDK `Display` instance.

198
src/modules/cairo.rs Normal file
View File

@ -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<CommonConfig>,
}
const fn default_size() -> u32 {
42
}
const fn default_frequency() -> u64 {
200
}
impl Module<gtk::Box> for CairoModule {
type SendMessage = ();
type ReceiveMessage = ();
module_impl!("cairo");
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_rx: Receiver<Self::ReceiveMessage>,
) -> color_eyre::Result<()>
where
<Self as Module<gtk::Box>>::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<Event>| 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<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleParts<gtk::Box>>
where
<Self as Module<gtk::Box>>::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<bool>>((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,
})
}
}

View File

@ -16,6 +16,8 @@ use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
use crate::popup::Popup; use crate::popup::Popup;
use crate::{glib_recv_mpsc, send, Ironbar}; use crate::{glib_recv_mpsc, send, Ironbar};
#[cfg(feature = "cairo")]
pub mod cairo;
#[cfg(feature = "clipboard")] #[cfg(feature = "clipboard")]
pub mod clipboard; pub mod clipboard;
/// Displays the current date and time. /// Displays the current date and time.