mirror of
https://github.com/JakeStanger/ironbar.git
synced 2024-11-22 05:34:35 +03:00
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:
parent
7b089495ad
commit
b0a05b7cda
56
Cargo.lock
generated
56
Cargo.lock
generated
@ -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"
|
||||
|
10
Cargo.toml
10
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"]
|
||||
@ -114,7 +117,12 @@ clap = { version = "4.5.4", optional = true, features = ["derive"] }
|
||||
serde_json = { version = "1.0.114", optional = true }
|
||||
|
||||
# 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
|
||||
nix = { version = "0.27.1", optional = true, features = ["event"] }
|
||||
|
@ -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. |
|
||||
|
@ -23,6 +23,7 @@
|
||||
|
||||
# Modules
|
||||
|
||||
- [Cairo](cairo)
|
||||
- [Clipboard](clipboard)
|
||||
- [Clock](clock)
|
||||
- [Custom](custom)
|
||||
|
215
docs/modules/Cairo.md
Normal file
215
docs/modules/Cairo.md
Normal 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
4
lua/draw.lua
Normal 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
4
lua/init.lua
Normal file
@ -0,0 +1,4 @@
|
||||
local lgi = require('lgi')
|
||||
cairo = lgi.cairo
|
||||
|
||||
__lgi_core = require('lgi.core')
|
41
src/clients/lua.rs
Normal file
41
src/clients/lua.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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<Arc<dyn compositor::WorkspaceClient>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: Option<Arc<clipboard::Client>>,
|
||||
#[cfg(feature = "cairo")]
|
||||
lua: Option<Rc<lua::LuaEngine>>,
|
||||
#[cfg(feature = "music")]
|
||||
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
|
||||
#[cfg(feature = "notifications")]
|
||||
@ -75,6 +81,13 @@ impl Clients {
|
||||
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")]
|
||||
pub fn music(&mut self, client_type: music::ClientType) -> Arc<dyn music::MusicClient> {
|
||||
self.music
|
||||
|
@ -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<CairoModule>),
|
||||
#[cfg(feature = "clipboard")]
|
||||
Clipboard(Box<ClipboardModule>),
|
||||
#[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")]
|
||||
|
39
src/main.rs
39
src/main.rs
@ -96,14 +96,18 @@ pub struct Ironbar {
|
||||
bars: Rc<RefCell<Vec<Bar>>>,
|
||||
clients: Rc<RefCell<Clients>>,
|
||||
config: Rc<RefCell<Config>>,
|
||||
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,13 +274,26 @@ 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,
|
||||
fn load_config() -> (Config, PathBuf) {
|
||||
let config_path = env::var("IRONBAR_CONFIG");
|
||||
|
||||
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")),
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
} 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/");
|
||||
@ -285,6 +302,10 @@ fn load_config() -> Config {
|
||||
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");
|
||||
|
||||
#[cfg(feature = "ipc")]
|
||||
@ -297,7 +318,7 @@ fn load_config() -> Config {
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
(config, directory)
|
||||
}
|
||||
|
||||
/// Gets the GDK `Display` instance.
|
||||
|
198
src/modules/cairo.rs
Normal file
198
src/modules/cairo.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user