From 22699852190fa40887bb2f05c8fa540cf3e158aa Mon Sep 17 00:00:00 2001 From: pepengu Date: Mon, 7 Jul 2025 21:18:32 +0300 Subject: [PATCH 1/6] feat: basic signals implemented --- src/root.rs | 11 ++++ src/util/mod.rs | 2 + src/util/signals.rs | 28 +++++++++++ tests/mod.rs | 1 + tests/util/mod.rs | 2 + tests/util/signals.rs | 114 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 158 insertions(+) create mode 100644 src/util/signals.rs create mode 100644 tests/mod.rs create mode 100644 tests/util/mod.rs create mode 100644 tests/util/signals.rs diff --git a/src/root.rs b/src/root.rs index e8e1145..5bd2055 100644 --- a/src/root.rs +++ b/src/root.rs @@ -1,6 +1,7 @@ use std::{ cell::RefCell, cmp::{max, min}, + collections::HashMap, num::NonZeroU32, rc::Rc, }; @@ -38,12 +39,14 @@ use wayland_client::{ pub struct Environment { pub config: Config, pub drawer: RefCell, + pub signals: RefCell>, } use crate::{ config::Config, util::{ fonts::{self, FontsError}, + signals::Signal, Drawer, }, widgets::{ @@ -428,8 +431,16 @@ impl Root { self.env = Some(Rc::new(Environment { config: Config::default(), drawer: RefCell::new(Drawer::new(&mut self.shm, 1, 1)), + signals: RefCell::new(HashMap::new()), })); + self.env + .as_ref() + .unwrap() + .signals + .borrow_mut() + .insert("test".to_string(), Signal::new()); + for widget in &mut self.widgets { widget.bind(Rc::clone(self.env.as_ref().unwrap()))?; } diff --git a/src/util/mod.rs b/src/util/mod.rs index df6aad7..3d1556a 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -5,3 +5,5 @@ pub mod drawer; pub use drawer::Drawer; pub mod fonts; + +pub mod signals; diff --git a/src/util/signals.rs b/src/util/signals.rs new file mode 100644 index 0000000..03c63b1 --- /dev/null +++ b/src/util/signals.rs @@ -0,0 +1,28 @@ +use std::any::Any; + +type Callback = Box; + +pub struct Signal { + listeners: Vec, +} + +impl Signal { + pub fn new() -> Self { + Signal { + listeners: Vec::new(), + } + } + + pub fn connect(&mut self, callback: F) + where + F: Fn(&dyn Any) + 'static, + { + self.listeners.push(Box::new(callback)); + } + + pub fn emit(&self, value: &T) { + for callback in &self.listeners { + callback(value); + } + } +} diff --git a/tests/mod.rs b/tests/mod.rs new file mode 100644 index 0000000..83c8c0a --- /dev/null +++ b/tests/mod.rs @@ -0,0 +1 @@ +mod util; diff --git a/tests/util/mod.rs b/tests/util/mod.rs new file mode 100644 index 0000000..2c57d70 --- /dev/null +++ b/tests/util/mod.rs @@ -0,0 +1,2 @@ +mod color; +mod signals; diff --git a/tests/util/signals.rs b/tests/util/signals.rs new file mode 100644 index 0000000..c65f6ea --- /dev/null +++ b/tests/util/signals.rs @@ -0,0 +1,114 @@ +#[cfg(test)] +mod tests { + use std::cell::RefCell; + use std::rc::Rc; + + use capybar::util::signals::Signal; + + #[test] + fn basic_usage() { + let mut signal = Signal::new(); + let last_value = Rc::new(RefCell::new(None)); + let last_value_clone = Rc::clone(&last_value); + + signal.connect(move |data| { + if let Some(value) = data.downcast_ref::() { + *last_value_clone.borrow_mut() = Some(*value); + } + }); + + assert!(last_value.borrow().is_none()); + + signal.emit(&42i32); + assert_eq!(*last_value.borrow(), Some(42)); + + signal.emit(&100i32); + assert_eq!(*last_value.borrow(), Some(100)); + } + + #[test] + fn reacts_to_correct_type() { + let mut signal = Signal::new(); + let state = Rc::new(RefCell::new(0)); + let state_clone = Rc::clone(&state); + + signal.connect(move |data| { + if let Some(value) = data.downcast_ref::() { + *state_clone.borrow_mut() += value; + } + }); + + signal.emit(&42i32); + signal.emit(&"ignore"); + assert_eq!(*state.borrow(), 42); + } + + #[test] + fn ignores_wrong_type() { + let mut signal = Signal::new(); + let called = Rc::new(RefCell::new(true)); + let called_clone = Rc::clone(&called); + + signal.connect(move |data| { + if data.downcast_ref::().is_some() { + *called_clone.borrow_mut() = false; + } + }); + + signal.emit(&"not a bool"); + assert!(*called.borrow()); + } + + #[test] + fn latest_emit_applied() { + let mut signal = Signal::new(); + let value = Rc::new(RefCell::new(0)); + let value_clone = Rc::clone(&value); + + signal.connect(move |data| { + if let Some(v) = data.downcast_ref::() { + *value_clone.borrow_mut() = *v; + } + }); + + signal.emit(&1i32); + signal.emit(&2i32); + signal.emit(&3i32); + + assert_eq!(*value.borrow(), 3); + } + + #[test] + fn no_panic_on_no_listeners() { + let signal = Signal::new(); + signal.emit(&"test"); + } + + #[test] + fn mixed_types_in_callbacks() { + let mut signal = Signal::new(); + let int_state = Rc::new(RefCell::new(0)); + let string_state = Rc::new(RefCell::new(String::new())); + + let int_clone = Rc::clone(&int_state); + signal.connect(move |data| { + if let Some(v) = data.downcast_ref::() { + *int_clone.borrow_mut() += v; + } + }); + + let str_clone = Rc::clone(&string_state); + signal.connect(move |data| { + if let Some(s) = data.downcast_ref::<&str>() { + *str_clone.borrow_mut() = s.to_string(); + } + }); + + signal.emit(&10); + signal.emit(&"text"); + signal.emit(&20); + + assert_eq!(*int_state.borrow(), 30); + assert_eq!(*string_state.borrow(), "text"); + } +} From ff4fab7bbfacc0cee9f776dedb754e22d1d8a80d Mon Sep 17 00:00:00 2001 From: pepengu Date: Mon, 7 Jul 2025 21:19:13 +0300 Subject: [PATCH 2/6] feat: improved color + tests --- src/util/color.rs | 95 +++++++++++++++----- tests/util/color.rs | 205 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+), 21 deletions(-) create mode 100644 tests/util/color.rs diff --git a/src/util/color.rs b/src/util/color.rs index 59b7e31..e029551 100644 --- a/src/util/color.rs +++ b/src/util/color.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use serde::Deserialize; /// Color structure used in capy. Color is stored as an rgba value. -#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)] pub struct Color(u32); impl Display for Color { @@ -41,6 +41,23 @@ impl Color { Self(u32::from_le_bytes(*bytes)) } + pub fn from_rgba_f32(r: f32, g: f32, b: f32, a: f32) -> Option { + if !(0.0..=1.0).contains(&r) + || !(0.0..=1.0).contains(&g) + || !(0.0..=1.0).contains(&b) + || !(0.0..=1.0).contains(&a) + { + return None; + } + + Some(Self::from_rgba( + (r * 255.0).round() as u8, + (g * 255.0).round() as u8, + (b * 255.0).round() as u8, + (a * 255.0).round() as u8, + )) + } + pub const fn to_be_bytes(self) -> [u8; 4] { self.0.to_be_bytes() } @@ -49,6 +66,37 @@ impl Color { self.0.to_le_bytes() } + pub fn r(&self) -> u8 { + ((self.0 & 0xFF000000) >> 24) as u8 + } + + pub fn g(&self) -> u8 { + ((self.0 & 0x00FF0000) >> 16) as u8 + } + + pub fn b(&self) -> u8 { + ((self.0 & 0x0000FF00) >> 8) as u8 + } + + pub fn a(&self) -> u8 { + (self.0 & 0x000000FF) as u8 + } + + pub fn set_r(&mut self, a: u8) { + self.0 &= 0x00FFFFFF; + self.0 |= (a as u32) << 24; + } + + pub fn set_g(&mut self, a: u8) { + self.0 &= 0xFF00FFFF; + self.0 |= (a as u32) << 16; + } + + pub fn set_b(&mut self, a: u8) { + self.0 &= 0xFFFF00FF; + self.0 |= (a as u32) << 8; + } + pub fn set_a(&mut self, a: u8) { self.0 &= 0xFFFFFF00; self.0 |= a as u32; @@ -58,27 +106,32 @@ impl Color { let bg = background.to_be_bytes(); let fg = foreground.to_be_bytes(); - //TODO check if checking for a == 0 improves speed - - let bg = [ - bg[0] as f32 * bg[3] as f32, - bg[1] as f32 * bg[3] as f32, - bg[2] as f32 * bg[3] as f32, - bg[3] as f32, - ]; - let fg = [ - fg[0] as f32 * fg[3] as f32, - fg[1] as f32 * fg[3] as f32, - fg[2] as f32 * fg[3] as f32, - fg[3] as f32, - ]; - - let coef = 1.0 - fg[3] / 255.0; - let a = fg[3] + bg[3] * coef; + if fg[3] == 0 { + return *background; + } + if fg[3] == 255 { + return *foreground; + } + if bg[3] == 0 { + return *foreground; + } + + let bg_alpha = bg[3] as f32 / 255.0; + let fg_alpha = fg[3] as f32 / 255.0; + + let a = fg_alpha + bg_alpha * (1.0 - fg_alpha); + + let blend_channel = |fg_c: u8, bg_c: u8| -> u8 { + let fg_norm = fg_c as f32 / 255.0; + let bg_norm = bg_c as f32 / 255.0; + let blended = (fg_norm * fg_alpha) + (bg_norm * bg_alpha * (1.0 - fg_alpha)); + (blended / a * 255.0).round() as u8 + }; + Color::from_rgba( - ((fg[0] + bg[0] * coef) / a).floor() as u8, - ((fg[1] + bg[1] * coef) / a).floor() as u8, - ((fg[2] + bg[2] * coef) / a).floor() as u8, + blend_channel(fg[0], bg[0]), + blend_channel(fg[1], bg[1]), + blend_channel(fg[2], bg[2]), (a * 255.0).floor() as u8, ) } diff --git a/tests/util/color.rs b/tests/util/color.rs new file mode 100644 index 0000000..02acef3 --- /dev/null +++ b/tests/util/color.rs @@ -0,0 +1,205 @@ +#[cfg(test)] +mod tests { + use capybar::util::Color; + + #[test] + fn test_from_rgba() { + let c = Color::from_rgba(0x12, 0x34, 0x56, 0x78); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0x34); + assert_eq!(c.b(), 0x56); + assert_eq!(c.a(), 0x78); + } + + #[test] + fn test_from_hex() { + let c = Color::from_hex(0x12345678); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0x34); + assert_eq!(c.b(), 0x56); + assert_eq!(c.a(), 0x78); + } + + #[test] + fn test_from_be_bytes() { + let bytes = [0x12, 0x34, 0x56, 0x78]; + let c = Color::from_be_bytes(&bytes); + assert_eq!(c.to_be_bytes(), bytes); + } + + #[test] + fn test_from_le_bytes() { + let bytes = [0x78, 0x56, 0x34, 0x12]; + let c = Color::from_le_bytes(&bytes); + assert_eq!(c.to_le_bytes(), bytes); + } + + #[test] + fn test_from_rgba_f32_valid() { + let c = Color::from_rgba_f32(0.0, 0.5, 1.0, 0.0).unwrap(); + assert_eq!(c.r(), 0); + assert_eq!(c.g(), 128); + assert_eq!(c.b(), 255); + assert_eq!(c.a(), 0); + } + + #[test] + fn test_from_rgba_f32_out_of_range() { + assert!(Color::from_rgba_f32(-0.1, 0.0, 0.0, 0.0).is_none()); + assert!(Color::from_rgba_f32(1.1, 0.0, 0.0, 0.0).is_none()); + assert!(Color::from_rgba_f32(0.0, -1.0, 0.0, 0.0).is_none()); + assert!(Color::from_rgba_f32(0.0, 0.0, 2.0, 0.0).is_none()); + assert!(Color::from_rgba_f32(0.0, 0.0, 0.0, -0.5).is_none()); + assert!(Color::from_rgba_f32(0.0, 0.0, 0.0, 1.5).is_none()); + } + + #[test] + fn test_setters() { + let mut c = Color::from_rgba(0, 0, 0, 0); + + c.set_r(0x12); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0); + assert_eq!(c.b(), 0); + assert_eq!(c.a(), 0); + + c.set_g(0x34); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0x34); + assert_eq!(c.b(), 0); + assert_eq!(c.a(), 0); + + c.set_b(0x56); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0x34); + assert_eq!(c.b(), 0x56); + assert_eq!(c.a(), 0); + + c.set_a(0x78); + assert_eq!(c.r(), 0x12); + assert_eq!(c.g(), 0x34); + assert_eq!(c.b(), 0x56); + assert_eq!(c.a(), 0x78); + } + + #[test] + fn test_blending_edge_cases() { + let bg = Color::from_rgba(100, 150, 200, 255); + let fg = Color::from_rgba(0, 0, 0, 0); + assert_eq!(Color::blend_colors(&bg, &fg), bg); + + let fg = Color::from_rgba(50, 100, 150, 255); + assert_eq!(Color::blend_colors(&bg, &fg), fg); + + let bg = Color::from_rgba(0, 0, 0, 0); + let fg = Color::from_rgba(75, 125, 175, 128); + assert_eq!(Color::blend_colors(&bg, &fg), fg); + + let fg = Color::from_rgba(0, 0, 0, 0); + assert_eq!(Color::blend_colors(&bg, &fg), fg); + } + + #[test] + fn test_blending_accuracy() { + let white = Color::from_rgba(255, 255, 255, 255); + let gray = Color::from_rgba(128, 128, 128, 128); + let blended = Color::blend_colors(&white, &gray); + assert_eq!(blended, Color::from_rgba(191, 191, 191, 255)); + + let blue = Color::from_rgba(0, 0, 255, 255); + let red = Color::from_rgba(255, 0, 0, 128); + let blended = Color::blend_colors(&blue, &red); + assert_eq!(blended, Color::from_rgba(128, 0, 127, 255)); + + let bg = Color::from_rgba(100, 100, 100, 128); + let fg = Color::from_rgba(200, 200, 200, 128); + let blended = Color::blend_colors(&bg, &fg); + assert_eq!(blended, Color::from_rgba(167, 167, 167, 191)); + } + + #[test] + fn test_blend_semi_transparent() { + let bg = Color::from_rgba(100, 100, 100, 255); + let fg = Color::from_rgba(200, 200, 200, 128); + let blended = Color::blend_colors(&bg, &fg); + + assert!(blended.r() > 100 && blended.r() < 200); + assert!(blended.g() > 100 && blended.g() < 200); + assert!(blended.b() > 100 && blended.b() < 200); + } + + #[test] + fn test_blending_alpha_boundaries() { + let bg = Color::from_rgba(0, 0, 0, 255); + let fg = Color::from_rgba(255, 255, 255, 1); + let blended = Color::blend_colors(&bg, &fg); + assert_eq!(blended.a(), 255); + + let fg = Color::from_rgba(255, 0, 0, 254); + let blended = Color::blend_colors(&bg, &fg); + assert_eq!(blended, Color::from_rgba(254, 0, 0, 255)); + } + + #[test] + fn test_blending_extreme_values() { + let white = Color::from_rgba(255, 255, 255, 255); + let black = Color::from_rgba(0, 0, 0, 255); + assert_eq!(Color::blend_colors(&white, &black), black); + + let transparent = Color::from_rgba(0, 0, 0, 0); + let visible = Color::from_rgba(10, 20, 30, 40); + assert_eq!(Color::blend_colors(&transparent, &visible), visible); + + let bg = Color::from_rgba(255, 255, 255, 255); + let fg = Color::from_rgba(255, 255, 255, 255); + let blended = Color::blend_colors(&bg, &fg); + assert_eq!(blended, Color::from_rgba(255, 255, 255, 255)); + } + #[test] + fn test_no_overflow() { + let c = Color::from_rgba(255, 255, 255, 255); + assert_eq!(c.r(), 255); + assert_eq!(c.g(), 255); + assert_eq!(c.b(), 255); + assert_eq!(c.a(), 255); + + let c = Color::from_rgba(0, 0, 0, 0); + assert_eq!(c.r(), 0); + assert_eq!(c.g(), 0); + assert_eq!(c.b(), 0); + assert_eq!(c.a(), 0); + + let bg = Color::from_rgba(255, 255, 255, 255); + let fg = Color::from_rgba(0, 0, 0, 0); + let blended = Color::blend_colors(&bg, &fg); + assert_eq!(blended, bg); + } + + #[test] + fn test_display() { + assert_eq!( + format!("{}", Color::from_rgba(0x12, 0x34, 0x56, 0x78)), + "0x12345678" + ); + assert_eq!(format!("{}", Color::from_rgba(0, 0, 0, 0)), "0x00000000"); + assert_eq!( + format!("{}", Color::from_rgba(0xFF, 0xFF, 0xFF, 0xFF)), + "0xffffffff" + ); + assert_eq!( + format!("{}", Color::from_rgba(0xAB, 0xCD, 0xEF, 0x12)), + "0xabcdef12" + ); + } + + #[test] + fn test_byte_conversion_roundtrip() { + let original = Color::from_rgba(0x12, 0x34, 0x56, 0x78); + + let be_bytes = original.to_be_bytes(); + assert_eq!(Color::from_be_bytes(&be_bytes), original); + + let le_bytes = original.to_le_bytes(); + assert_eq!(Color::from_le_bytes(&le_bytes), original); + } +} From a2ed2895560b63199a5c6c783edb7b851eb8f580 Mon Sep 17 00:00:00 2001 From: pepengu Date: Mon, 7 Jul 2025 22:50:49 +0300 Subject: [PATCH 3/6] docs: signals explained --- src/util/signals.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/util/signals.rs b/src/util/signals.rs index 03c63b1..27c11db 100644 --- a/src/util/signals.rs +++ b/src/util/signals.rs @@ -2,6 +2,38 @@ use std::any::Any; type Callback = Box; +/// Signal is a mechanism that allows communication between different components +/// +/// Signal can be treated as a tunnel. You can pass values through the tunnel and +/// have functions that react to those values. This allows to split heavy procceses +/// from displaing the results. This is done by alowing to bind function that can +/// react to a passed data. +/// +/// Downcasting and validating data is left to user. You are only provided with +/// `dyn Any`. +/// +/// Whenever signal emit is called all the functions connected to a signal are called. +/// +/// In capybar signals are stored inside an [Environment](crate::root::Environment) +/// in a `signals` [HashMap](std::collections::HashMap) +/// +/// # Examples +/// +/// ``` +/// let mut signal = Signal::new(); +/// let last_value = Rc::new(RefCell::new(None)); +/// let last_value_clone = Rc::clone(&last_value); +/// +/// signal.connect(move |data| { +/// if let Some(value) = data.downcast_ref::() { +/// *last_value_clone.borrow_mut() = Some(*value); +/// } +/// }); +/// +/// //... +/// +/// signal.emit(&42i32); +/// ``` pub struct Signal { listeners: Vec, } From 73c2282004322a3526737320b741672cce674a70 Mon Sep 17 00:00:00 2001 From: pepengu Date: Mon, 14 Jul 2025 13:16:51 +0300 Subject: [PATCH 4/6] feat: keyboard widget and process --- Cargo.lock | 398 ++++++++++++++++++++- Cargo.toml | 13 + examples/basic/main.rs | 3 +- examples/toml_config/README.md | 21 ++ examples/toml_config/config.toml | 11 +- examples/toml_config/main.rs | 2 +- src/lib.rs | 5 +- src/processes/clients/hyprland/keyboard.rs | 116 ++++++ src/processes/clients/hyprland/mod.rs | 3 + src/processes/clients/mod.rs | 13 + src/processes/mod.rs | 51 +++ src/root.rs | 84 +++-- src/util/signals.rs | 14 +- src/widgets/cpu.rs | 6 +- src/widgets/keyboard.rs | 167 +++++++++ src/widgets/mod.rs | 18 +- 16 files changed, 870 insertions(+), 55 deletions(-) create mode 100644 examples/toml_config/README.md create mode 100644 src/processes/clients/hyprland/keyboard.rs create mode 100644 src/processes/clients/hyprland/mod.rs create mode 100644 src/processes/clients/mod.rs create mode 100644 src/processes/mod.rs create mode 100644 src/widgets/keyboard.rs diff --git a/Cargo.lock b/Cargo.lock index b9b1153..ea294b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,34 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -29,12 +57,49 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "battery" version = "0.7.8" @@ -90,6 +155,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "calloop" version = "0.13.0" @@ -125,6 +196,7 @@ dependencies = [ "chrono", "fontconfig", "fontdue", + "hyprland", "serde", "smithay-client-toolkit", "sysinfo", @@ -206,6 +278,27 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "dlib" version = "0.5.2" @@ -221,6 +314,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -234,7 +333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -262,6 +361,28 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "hashbrown" version = "0.15.3" @@ -279,6 +400,39 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +[[package]] +name = "hyprland" +version = "0.4.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9c1413b6f0fd10b2e4463479490e30b2497ae4449f044da16053f5f2cb03b8" +dependencies = [ + "ahash", + "async-stream", + "derive_more", + "either", + "futures-lite", + "hyprland-macros", + "num-traits", + "once_cell", + "paste", + "phf", + "serde", + "serde_json", + "serde_repr", + "tokio", +] + +[[package]] +name = "hyprland-macros" +version = "0.4.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e3cbed6e560408051175d29a9ed6ad1e64a7ff443836addf797b0479f58983" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -313,6 +467,23 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -396,6 +567,26 @@ dependencies = [ "libc", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.59.0", +] + [[package]] name = "nix" version = "0.19.1" @@ -445,12 +636,69 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -475,7 +723,7 @@ dependencies = [ "pin-project-lite", "rustix 1.0.7", "tracing", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -505,6 +753,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + [[package]] name = "rustix" version = "0.38.44" @@ -515,7 +784,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -528,7 +797,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -537,6 +806,12 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "serde" version = "1.0.219" @@ -557,6 +832,29 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -572,6 +870,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -615,6 +919,16 @@ dependencies = [ "xkeysym", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "syn" version = "2.0.101" @@ -680,6 +994,35 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio" +version = "1.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "toml" version = "0.8.23" @@ -755,6 +1098,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "uom" version = "0.30.0" @@ -765,6 +1114,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1039,6 +1400,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1230,3 +1600,23 @@ dependencies = [ "once_cell", "pkg-config", ] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 2bbee13..3295b02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,17 @@ version = "0.1.1" edition = "2021" license = "MIT" +[features] +default = [ + "keyboard+all" +] + +hyprland = [] + +keyboard = [] +"keyboard+hyprland" = ["keyboard", "hyprland"] +"keyboard+all" = ["keyboard", "keyboard+hyprland"] + [dependencies] #Wayland handling smithay-client-toolkit = "0.19.2" @@ -28,3 +39,5 @@ chrono = "0.4.41" battery = "0.7.8" #CPU sysinfo = "0.35.1" + +hyprland = "0.4.0-beta" diff --git a/examples/basic/main.rs b/examples/basic/main.rs index 0a31898..690c3c3 100644 --- a/examples/basic/main.rs +++ b/examples/basic/main.rs @@ -1,4 +1,5 @@ use capybar::{ + root::Root, util::Color, widgets::{ battery::{Battery, BatterySettings}, @@ -8,7 +9,6 @@ use capybar::{ text::TextSettings, Style, WidgetData, WidgetNew, }, - Root, }; use wayland_client::{globals::registry_queue_init, Connection}; @@ -60,6 +60,7 @@ fn main() -> Result<(), Box> { bar.create_child_left( CPU::new, CPUSettings { + update_rate: 1000, text_settings: TextSettings { font_color: catpuccin_mocha.font, size: 25.0, diff --git a/examples/toml_config/README.md b/examples/toml_config/README.md new file mode 100644 index 0000000..92a15ff --- /dev/null +++ b/examples/toml_config/README.md @@ -0,0 +1,21 @@ +# Toml config +Contains toml config bar implementaition. +The config is located in [config.toml](https://github.com/CapyCore/capybar/blob/master/examples/toml_config/config.toml) file + +Example uses system's mono font for text and JetBrainsNerd font for emoji like battery symbol + +## The toml config example looks like this: +![Screenshot of the toml_config example bar](./bar.png) +## Usage +Build the example with +``` +cargo build --release --example toml_config +``` +Then find executable at ```capybar/target/release/example/toml_config``` + +Or run the example with +``` +cargo run --release --example toml_config +``` + + diff --git a/examples/toml_config/config.toml b/examples/toml_config/config.toml index 5fa651d..f8d02d6 100644 --- a/examples/toml_config/config.toml +++ b/examples/toml_config/config.toml @@ -12,13 +12,22 @@ width = 1920 background = 0x1e1e2eff border = [1, 0x74c7ecff] +[[bar.left]] + widget = "keyboard" + [[bar.left.settings]] + size = 24 + font_color = 0xf5e0dcff + margin = [10,0,0,0] + layout_mappings = {"Russian" = "RU", "English (US)" = "EN"} + [[bar.left.settings]] + update_rate = 0 + [[bar.left]] widget = "cpu" [bar.left.settings] size = 24 font_color = 0xf5e0dcff margin = [10,0,0,0] - update_rate = 1000 [[bar.center]] widget = "clock" diff --git a/examples/toml_config/main.rs b/examples/toml_config/main.rs index 5fe862d..6979e10 100644 --- a/examples/toml_config/main.rs +++ b/examples/toml_config/main.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use capybar::{config::Config, Root}; +use capybar::{config::Config, root::Root}; use wayland_client::{globals::registry_queue_init, Connection}; fn main() -> Result<()> { diff --git a/src/lib.rs b/src/lib.rs index b193ec6..5ce7486 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ pub mod config; +pub mod processes; +pub mod root; pub mod util; pub mod widgets; - -pub mod root; -pub use root::Root; diff --git a/src/processes/clients/hyprland/keyboard.rs b/src/processes/clients/hyprland/keyboard.rs new file mode 100644 index 0000000..93558c3 --- /dev/null +++ b/src/processes/clients/hyprland/keyboard.rs @@ -0,0 +1,116 @@ +use std::{cell::RefCell, rc::Rc}; + +use anyhow::anyhow; +use chrono::{DateTime, Duration, Local}; +use hyprland::{data::Devices, shared::HyprData}; + +use crate::{ + processes::{clients::KeyboardTrait, Process, ProcessNew, ProcessSettings}, + root::Environment, + util::signals::Signal, +}; + +/// Process that tracks current keyboard layout +pub struct Keyboard { + settings: ProcessSettings, + + last_layout: RefCell, + last_update: RefCell>, + + env: Option>, +} + +impl Process for Keyboard { + fn bind(&mut self, env: std::rc::Rc) -> anyhow::Result<()> { + self.env = Some(Rc::clone(&env)); + Ok(()) + } + + fn init(&self) -> anyhow::Result<()> { + if self.env.is_none() { + return Err(anyhow!("No Environment is bound")); + } + + let mut signals = self.env.as_ref().unwrap().signals.borrow_mut(); + if !signals.contains_key("keyboard") { + signals.insert("keyboard".to_string(), Signal::new()); + } + + let keyboards = Devices::get()?.keyboards; + if keyboards.is_empty() { + return Err(anyhow!("No Keyboard connected")); + } + + let mut last_layout = self.last_layout.borrow_mut(); + for keyboard in keyboards { + if keyboard.main { + *last_layout = keyboard.layout; + } + } + + signals["keyboard"].emit(&last_layout.clone()); + + Ok(()) + } + + fn run(&self) -> anyhow::Result<()> { + if self.env.is_none() { + return Err(anyhow!("No Environment is bound")); + } + + let mut last_update = self.last_update.borrow_mut(); + if Local::now() - *last_update < Duration::milliseconds(self.settings.update_rate) { + return Ok(()); + } + *last_update = Local::now(); + + let keyboards = Devices::get()?.keyboards; + + if keyboards.is_empty() { + return Err(anyhow!("No Keyboard connected")); + } + + let signals = self.env.as_ref().unwrap().signals.borrow_mut(); + let mut last_layout = self.last_layout.borrow_mut(); + for keyboard in keyboards { + if keyboard.main && *last_layout != keyboard.active_keymap { + *last_layout = keyboard.active_keymap; + signals["keyboard"].emit(&last_layout.clone()); + } + } + + Ok(()) + } +} + +impl ProcessNew for Keyboard { + type Settings = ProcessSettings; + + fn new( + env: Option>, + settings: Self::Settings, + ) -> anyhow::Result + where + Self: Sized, + { + let keyboards = Devices::get()?.keyboards; + if keyboards.is_empty() { + return Err(anyhow!("No Keyboard connected")); + } + + for keyboard in keyboards { + if keyboard.main { + return Ok(Keyboard { + settings, + last_update: RefCell::new(DateTime::default()), + last_layout: RefCell::new(String::new()), + env, + }); + } + } + + return Err(anyhow!("No main keyboard found")); + } +} + +impl KeyboardTrait for Keyboard {} diff --git a/src/processes/clients/hyprland/mod.rs b/src/processes/clients/hyprland/mod.rs new file mode 100644 index 0000000..9123b57 --- /dev/null +++ b/src/processes/clients/hyprland/mod.rs @@ -0,0 +1,3 @@ +//! Current module describes all of the hyprland communication + +pub mod keyboard; diff --git a/src/processes/clients/mod.rs b/src/processes/clients/mod.rs new file mode 100644 index 0000000..9a9e612 --- /dev/null +++ b/src/processes/clients/mod.rs @@ -0,0 +1,13 @@ +//! Current module describes all of capybars clients. Different compositors handle some stuff +//! differently. All of the unique behaviours is described here. + +use super::Process; + +#[cfg(feature = "hyprland")] +pub mod hyprland; + +#[cfg(feature = "keyboard")] +trait KeyboardTrait: Process {} + +#[cfg(feature = "keyboard+hyprland")] +pub use hyprland::keyboard::Keyboard; diff --git a/src/processes/mod.rs b/src/processes/mod.rs new file mode 100644 index 0000000..871c56c --- /dev/null +++ b/src/processes/mod.rs @@ -0,0 +1,51 @@ +//! Current module describes all of capybars included processes as well as their common behaviour. +//! +//! Process can be treated as a backend component. +//! To communicate with frontend you can use [Signal](crate::util::signals::Signal) + +pub mod clients; + +use std::rc::Rc; + +use anyhow::Result; +use serde::Deserialize; +use thiserror::Error; + +use crate::root::Environment; + +fn default_update_rate() -> i64 { + return 1000; +} + +#[derive(Debug, Deserialize, Clone, Copy)] +pub struct ProcessSettings { + #[serde(default = "default_update_rate")] + pub update_rate: i64, +} + +/// A **data structure** that can be used as a widget inside a capybar. +pub trait Process { + /// Bind a widget to a new environment. + fn bind(&mut self, env: Rc) -> Result<()>; + + /// Prepare `Process` for a first run + fn init(&self) -> Result<()>; + + /// Run the process + fn run(&self) -> Result<()>; +} + +/// A `Process` that can be unifiedly created. +/// +/// Implementing this trait allows creating `Process` and binding the environment without +/// intermidiate steps. Simplifies process creation inside of scripts. +pub trait ProcessNew { + type Settings; + + fn new(env: Option>, settings: Self::Settings) -> Result + where + Self: Sized; +} + +#[derive(Debug, Error)] +pub enum ProcessError {} diff --git a/src/root.rs b/src/root.rs index 5bd2055..c47b5f5 100644 --- a/src/root.rs +++ b/src/root.rs @@ -4,6 +4,8 @@ use std::{ collections::HashMap, num::NonZeroU32, rc::Rc, + thread, + time::Duration, }; use anyhow::Result; @@ -35,26 +37,27 @@ use wayland_client::{ Connection, EventQueue, QueueHandle, }; -/// Structure containing things all the widgets in capybar needs access to -pub struct Environment { - pub config: Config, - pub drawer: RefCell, - pub signals: RefCell>, -} - use crate::{ config::Config, + processes::{clients, Process, ProcessNew}, util::{ fonts::{self, FontsError}, signals::Signal, Drawer, }, widgets::{ - battery::Battery, clock::Clock, containers::bar::Bar, cpu::CPU, text::Text, Widget, - WidgetNew, WidgetsList, + self, battery::Battery, clock::Clock, containers::bar::Bar, cpu::CPU, keyboard::Keyboard, + text::Text, Widget, WidgetNew, WidgetsList, }, }; +/// Structure containing things all the widgets in capybar needs access to +pub struct Environment { + pub config: Config, + pub drawer: RefCell, + pub signals: RefCell>, +} + #[derive(Error, Debug)] pub enum RootError { #[error("Environment is not initialised before drawing")] @@ -69,7 +72,6 @@ pub struct Root { output_state: OutputState, shm: Shm, - exit: bool, first_configure: bool, width: u32, height: u32, @@ -80,6 +82,7 @@ pub struct Root { pointer: Option, widgets: Vec>, + processes: Vec>, env: Option>, } @@ -168,9 +171,7 @@ impl OutputHandler for Root { } impl LayerShellHandler for Root { - fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle, _layer: &LayerSurface) { - self.exit = true; - } + fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle, _layer: &LayerSurface) {} fn configure( &mut self, @@ -278,11 +279,8 @@ impl KeyboardHandler for Root { _qh: &QueueHandle, _: &wl_keyboard::WlKeyboard, _: u32, - event: KeyEvent, + _: KeyEvent, ) { - if event.keysym == Keysym::Escape { - self.exit = true; - } } fn release_key( @@ -368,7 +366,6 @@ impl Root { output_state: OutputState::new(globals, &qh), shm, - exit: false, first_configure: true, width: 16, height: 16, @@ -379,6 +376,7 @@ impl Root { pointer: None, widgets: Vec::new(), + processes: Vec::new(), env: None, }; @@ -394,6 +392,10 @@ impl Root { WidgetsList::Clock(settings) => bar.create_child_left(Clock::new, settings)?, WidgetsList::Battery(settings) => bar.create_child_left(Battery::new, settings)?, WidgetsList::CPU(settings) => bar.create_child_left(CPU::new, settings)?, + WidgetsList::Keyboard(wsettings, psettings) => { + self.create_process(clients::Keyboard::new, psettings)?; + bar.create_child_left(Keyboard::new, wsettings)? + } } } @@ -405,6 +407,10 @@ impl Root { bar.create_child_center(Battery::new, settings)? } WidgetsList::CPU(settings) => bar.create_child_center(CPU::new, settings)?, + WidgetsList::Keyboard(wsettings, psettings) => { + self.create_process(clients::Keyboard::new, psettings)?; + bar.create_child_center(widgets::keyboard::Keyboard::new, wsettings)? + } } } @@ -414,6 +420,10 @@ impl Root { WidgetsList::Clock(settings) => bar.create_child_right(Clock::new, settings)?, WidgetsList::Battery(settings) => bar.create_child_right(Battery::new, settings)?, WidgetsList::CPU(settings) => bar.create_child_right(CPU::new, settings)?, + WidgetsList::Keyboard(wsettings, psettings) => { + self.create_process(clients::Keyboard::new, psettings)?; + bar.create_child_right(widgets::keyboard::Keyboard::new, wsettings)? + } } } @@ -425,8 +435,8 @@ impl Root { self.layer.set_anchor(Anchor::TOP); self.layer .set_keyboard_interactivity(KeyboardInteractivity::OnDemand); - self.width = 0; - self.height = 0; + self.width = 1; + self.height = 1; self.env = Some(Rc::new(Environment { config: Config::default(), @@ -434,18 +444,15 @@ impl Root { signals: RefCell::new(HashMap::new()), })); - self.env - .as_ref() - .unwrap() - .signals - .borrow_mut() - .insert("test".to_string(), Signal::new()); + for process in &mut self.processes { + process.bind(Rc::clone(self.env.as_ref().unwrap()))?; - for widget in &mut self.widgets { - widget.bind(Rc::clone(self.env.as_ref().unwrap()))?; + process.init()?; } for widget in &mut self.widgets { + widget.bind(Rc::clone(self.env.as_ref().unwrap()))?; + widget.init()?; let data = widget.data().borrow_mut(); self.height = max( @@ -487,13 +494,10 @@ impl Root { loop { event_queue.blocking_dispatch(self)?; - - if self.exit { - break; - } + thread::sleep(Duration::from_millis(100)); } - Ok(self) + //Ok(self) } pub fn add_font_by_name(&mut self, name: &'static str) -> Result<(), FontsError> { @@ -520,11 +524,25 @@ impl Root { Ok(()) } + pub fn create_process(&mut self, f: F, settings: W::Settings) -> Result<()> + where + W: ProcessNew + Process + 'static, + F: FnOnce(Option>, W::Settings) -> Result, + { + self.processes + .push(Box::new(f(self.env.clone(), settings)?)); + Ok(()) + } + fn draw(&mut self, qh: &QueueHandle) -> Result<()> { if self.env.is_none() { return Err(RootError::EnvironmentNotInit.into()); } + for process in &mut self.processes { + process.run()?; + } + self.layer .wl_surface() .damage_buffer(0, 0, self.width as i32, self.height as i32); diff --git a/src/util/signals.rs b/src/util/signals.rs index 27c11db..a3f613b 100644 --- a/src/util/signals.rs +++ b/src/util/signals.rs @@ -1,4 +1,4 @@ -use std::any::Any; +use std::{any::Any, cell::RefCell}; type Callback = Box; @@ -9,7 +9,7 @@ type Callback = Box; /// from displaing the results. This is done by alowing to bind function that can /// react to a passed data. /// -/// Downcasting and validating data is left to user. You are only provided with +/// Downcasting and validating data is left to user. You are only provided with /// `dyn Any`. /// /// Whenever signal emit is called all the functions connected to a signal are called. @@ -35,25 +35,25 @@ type Callback = Box; /// signal.emit(&42i32); /// ``` pub struct Signal { - listeners: Vec, + listeners: RefCell>, } impl Signal { pub fn new() -> Self { Signal { - listeners: Vec::new(), + listeners: RefCell::new(Vec::new()), } } - pub fn connect(&mut self, callback: F) + pub fn connect(&self, callback: F) where F: Fn(&dyn Any) + 'static, { - self.listeners.push(Box::new(callback)); + self.listeners.borrow_mut().push(Box::new(callback)); } pub fn emit(&self, value: &T) { - for callback in &self.listeners { + for callback in &*self.listeners.borrow_mut() { callback(value); } } diff --git a/src/widgets/cpu.rs b/src/widgets/cpu.rs index 94dc75b..b74b8f8 100644 --- a/src/widgets/cpu.rs +++ b/src/widgets/cpu.rs @@ -38,7 +38,7 @@ pub struct CPU { sys: RefCell, last_update: RefCell>, - upadte_rate: TimeDelta, + update_rate: TimeDelta, } impl CPU { @@ -94,7 +94,7 @@ impl Widget for CPU { fn draw(&self) -> Result<()> { let mut last_update = self.last_update.borrow_mut(); - if Local::now() - *last_update >= self.upadte_rate { + if Local::now() - *last_update >= self.update_rate { let info = self.get_info(); { @@ -161,7 +161,7 @@ impl WidgetNew for CPU { RefreshKind::nothing().with_cpu(CpuRefreshKind::nothing().with_cpu_usage()), )), - upadte_rate: TimeDelta::milliseconds(settings.update_rate as i64), + update_rate: TimeDelta::milliseconds(settings.update_rate as i64), last_update: RefCell::new( chrono::Local::now() - TimeDelta::milliseconds(settings.update_rate as i64), ), diff --git a/src/widgets/keyboard.rs b/src/widgets/keyboard.rs new file mode 100644 index 0000000..4aeb64f --- /dev/null +++ b/src/widgets/keyboard.rs @@ -0,0 +1,167 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use anyhow::Result; +use serde::Deserialize; + +use crate::{ + root::Environment, + widgets::{text::Text, Widget}, +}; + +use super::{text::TextSettings, Style, WidgetData, WidgetError, WidgetNew}; + +/// Settings of a [Keyboard] widget +#[derive(Deserialize, Default, Debug, Clone)] +pub struct KeyboardSettings { + #[serde(default, flatten)] + pub default_data: WidgetData, + + /// Settings for underlying [Text] widget + #[serde(default, flatten)] + pub text_settings: TextSettings, + + #[serde(default, flatten)] + pub style: Style, + + /// Map from underlying layout name to display name + #[serde(default)] + pub layout_mappings: HashMap, +} + +/// Widget displaying current keyboard layout. +pub struct Keyboard { + data: RefCell, + layout_mappings: Rc>, + + icon: RefCell, + text: Rc>, + + env: Option>, +} + +impl Keyboard { + fn align(&self) -> Result<()> { + let icon = self.icon.borrow_mut(); + let text = self.text.borrow_mut(); + + let mut icon_data = icon.data().borrow_mut(); + let mut text_data = text.data().borrow_mut(); + let data = &mut self.data.borrow_mut(); + + icon_data.position.0 = data.position.0 + icon_data.margin.0; + icon_data.position.1 = data.position.1 + icon_data.margin.2; + text_data.position.0 = + icon_data.position.0 + icon_data.width + icon_data.margin.1 + text_data.margin.0; + text_data.position.1 = data.position.1 + text_data.margin.2; + + data.height = usize::max( + text_data.position.1 + text_data.height + text_data.margin.3, + icon_data.position.1 + icon_data.height + icon_data.margin.3, + ); + + data.width = icon_data.margin.0 + + icon_data.margin.1 + + icon_data.width + + text_data.margin.0 + + text_data.margin.1 + + text_data.width; + + Ok(()) + } +} + +impl Widget for Keyboard { + fn bind(&mut self, env: Rc) -> Result<()> { + self.env = Some(env.clone()); + self.text.borrow_mut().bind(env.clone())?; + self.icon.borrow_mut().bind(env) + } + + fn init(&self) -> Result<()> { + if self.env.is_none() { + return Err(WidgetError::InitWithNoEnv("Keyboard".to_string()).into()); + } + + let signals = self.env.as_ref().unwrap().signals.borrow_mut(); + + if !signals.contains_key("keyboard") { + return Err(WidgetError::NoCorespondingSignal( + "Keyboard".to_string(), + "Keyboard".to_string(), + ) + .into()); + } + + let signal_text = Rc::clone(&self.text); + let layout_mappings = Rc::clone(&self.layout_mappings); + + signals["keyboard"].connect(move |data| { + if let Some(text) = data.downcast_ref::() { + let layout = if layout_mappings.contains_key(text) { + layout_mappings.get(text).unwrap() + } else { + &text.to_string() + }; + + signal_text.borrow_mut().change_text(&layout); + } + }); + + self.icon.borrow_mut().init()?; + self.text.borrow_mut().init()?; + + self.align() + } + + fn draw(&self) -> Result<()> { + self.align()?; + + self.text.borrow_mut().draw()?; + self.icon.borrow_mut().draw() + } + + fn data(&self) -> &RefCell { + &self.data + } +} + +impl WidgetNew for Keyboard { + type Settings = KeyboardSettings; + + fn new(env: Option>, settings: Self::Settings) -> Result + where + Self: Sized, + { + Ok(Keyboard { + data: RefCell::new(settings.default_data), + layout_mappings: Rc::new(settings.layout_mappings), + + icon: RefCell::new(Text::new( + env.clone(), + TextSettings { + text: "󰌌".to_string(), + default_data: WidgetData { + margin: (0, 0, 0, 0), + ..WidgetData::default() + }, + fontid: 1, + ..settings.text_settings.clone() + }, + )?), + text: Rc::new(RefCell::new(Text::new( + env, + TextSettings { + text: String::new(), + + default_data: WidgetData { + margin: (5, 0, 2, 0), + ..WidgetData::default() + }, + ..settings.text_settings.clone() + }, + )?)), + + env: None, + }) + } +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index ad23519..aedf818 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -3,6 +3,7 @@ pub mod containers; pub mod battery; pub mod clock; pub mod cpu; +pub mod keyboard; pub mod text; use std::{cell::RefCell, rc::Rc}; @@ -11,7 +12,7 @@ use anyhow::Result; use serde::Deserialize; use thiserror::Error; -use crate::{root::Environment, util::Color}; +use crate::{processes::ProcessSettings, root::Environment, util::Color}; use {battery::BatterySettings, clock::ClockSettings, cpu::CPUSettings, text::TextSettings}; @@ -20,7 +21,7 @@ pub trait Widget { /// Bind a widget to a new environment. fn bind(&mut self, env: Rc) -> Result<()>; - /// Draw an entire widget to a `Drawer` + /// Draw an entire widget to a current environment's `Drawer` fn draw(&self) -> Result<()>; /// Prepare `Widget` for a first draw @@ -47,8 +48,20 @@ pub enum WidgetError { #[error("Invalid widget bounds")] InvalidBounds, + /// Argument is a name of a widget #[error("Trying to draw a widget \"{0}\" not bound to any environment")] DrawWithNoEnv(String), + + /// Argument is a name of a widget + #[error("Trying to initialise a widget \"{0}\" not bound to any environment")] + InitWithNoEnv(String), + + /// Arguments are a name of a widget and a name of coresponding process + #[error( + "When initialising widget \"{0}\" no coresponding signal was found. + Maybe process \"{1}\" was not created?" + )] + NoCorespondingSignal(String, String), } /// Global common data used by `Widget` data structure. @@ -128,4 +141,5 @@ pub enum WidgetsList { Battery(BatterySettings), #[serde(rename = "cpu")] CPU(CPUSettings), + Keyboard(keyboard::KeyboardSettings, ProcessSettings), } From b0ed36028cc4ea3f78156df7210163083d13b6e6 Mon Sep 17 00:00:00 2001 From: pepengu Date: Mon, 14 Jul 2025 19:08:34 +0300 Subject: [PATCH 5/6] feat: basic nix packaging --- .github/workflows/nix.yml | 18 +++ Cargo.lock | 127 +++++++++++++++++++++ Cargo.toml | 5 + README.md | 36 +++++- nix/flake.lock => flake.lock | 12 +- nix/flake.nix => flake.nix | 32 +++--- nix/module.nix | 26 +++++ src/main.rs | 92 +++++++++++++++ src/processes/clients/hyprland/keyboard.rs | 2 +- src/processes/clients/mod.rs | 1 + src/processes/mod.rs | 2 +- src/util/signals.rs | 26 +++-- src/widgets/keyboard.rs | 2 +- tests/util/signals.rs | 10 +- 14 files changed, 350 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/nix.yml rename nix/flake.lock => flake.lock (77%) rename nix/flake.nix => flake.nix (74%) create mode 100644 nix/module.nix create mode 100644 src/main.rs diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..e2452d1 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,18 @@ +on: + pull_request: + branches: [ master, dev ] + push: + branches: [ master, dev ] + +jobs: + build: + name: Build Nix targets + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Install Nix + uses: DeterminateSystems/determinate-nix-action@v3 + with: + fail-mode: true + - name: Build default package + run: nix build diff --git a/Cargo.lock b/Cargo.lock index ea294b6..e5d585c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -194,6 +244,7 @@ dependencies = [ "anyhow", "battery", "chrono", + "clap", "fontconfig", "fontdue", "hyprland", @@ -235,6 +286,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -394,6 +491,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.1" @@ -478,6 +581,12 @@ dependencies = [ "libc", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.15" @@ -651,6 +760,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "paste" version = "1.0.15" @@ -929,6 +1044,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.101" @@ -1114,6 +1235,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 3295b02..a7ba6a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,10 @@ [package] name = "capybar" +description = "Wayland native toolbar" version = "0.1.1" edition = "2021" license = "MIT" +repository = "https://github.com/CapyCore/capybar" [features] default = [ @@ -29,6 +31,9 @@ anyhow = "1.0.98" toml = "0.8.23" serde = {version = "1.0.219", features = [ "derive" ] } +#CLI arguments +clap = {version = "4.5.41", features = [ "derive" ]} + ### Widget dependencies #Fonts fontconfig = "0.9.0" diff --git a/README.md b/README.md index 09d6116..6c67fd9 100644 --- a/README.md +++ b/README.md @@ -13,20 +13,48 @@ Simple customizable bar applications that aims to have as little external depend - Clock - Battery - CPU usage + - Keyboard layout - Row container (WIP) - Bar container ## Instalation -Currently bar needs to be build manually. To do so clone the repo and write main file. Bulding the bar is done with cargo. The example is located in examples folder. To use the basic example run: + +### Nix + +Capybar can be installed on nix using home manager. +- Extend your inputs with: +```nix + inputs = { + # ... + capybar.url = "github:CapyCore/capybar"; + }; ``` -cargo build --release --example basic + +- Extend your imports with: +```nix +imports = [ inputs.capybar.homeManagerModules.default ]; +``` + +- Enable capybar: +```nix +programs.capybar = { + enable = true; +} +``` + +### Others +Currently bar needs to be build manually. To do so clone the repo and write main file. Bulding the bar is done with cargo. The example is located in examples folder. +``` +cargo build --release ``` -## Usage After building the bar the executable will be located in `./target/release/` -The basic example exetutable is `./target/release/examples/basic` +## Usage +Capybar can be run using `capybar` command in a terminal of your choice. You can change configuration path via flag +`--cfg_path` (default path is `$HOME/.config/capybar`) and config extention via `--cfg_type` (default is toml, no other types are +currently supported). More info could be accesed wit `--help` flag. ## License diff --git a/nix/flake.lock b/flake.lock similarity index 77% rename from nix/flake.lock rename to flake.lock index de82eae..66b4127 100644 --- a/nix/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1746810718, - "narHash": "sha256-VljtYzyttmvkWUKTVJVW93qAsJsrBbgAzy7DdnJaQfI=", + "lastModified": 1751274312, + "narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0c0bf9c057382d5f6f63d54fd61f1abd5e1c2f63", + "rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674", "type": "github" }, "original": { @@ -43,11 +43,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1746931022, - "narHash": "sha256-cXn1RsYZjS23n0+YP3TiH7XBlEvy8FA2mG54MdAL6x0=", + "lastModified": 1752461263, + "narHash": "sha256-f4XVgqkWF1vSzPbOG5xvi4aAd/n1GwSNsji3mLMFwYQ=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "c46d2764319f962b20ce9c03ce6644dd0de87bc9", + "rev": "9cc51d100d24fb7ea13a0bee1480ee84fa12a0ad", "type": "github" }, "original": { diff --git a/nix/flake.nix b/flake.nix similarity index 74% rename from nix/flake.nix rename to flake.nix index e44cc8b..c95be5a 100644 --- a/nix/flake.nix +++ b/flake.nix @@ -1,8 +1,8 @@ { - description = "kapibar"; + description = "capybar"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; rust-overlay.url = "github:oxalica/rust-overlay"; }; @@ -25,30 +25,35 @@ rustc = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default); }; - manifest = (pkgs.lib.importTOML ./crates/kapibar/Cargo.toml).package; + manifest = (pkgs.lib.importTOML ./Cargo.toml).package; in rustPlatform.buildRustPackage { pname = manifest.name; inherit (manifest) version; buildInputs = with pkgs; [ + fontconfig libxkbcommon - cairo - libpulseaudio ]; nativeBuildInputs = with pkgs; [ pkg-config ]; - + cargoLock = { lockFile = ./Cargo.lock; allowBuiltinFetchGit = true; }; src = pkgs.lib.cleanSource ./.; - - RUSTFLAGS = "--cfg tokio_unstable"; + + meta = { + description = "Native wayland toolbar"; + homepage = "https://github.com/CapyCore/capybar"; + platforms = nixpkgs.lib.platforms.linux; + license = nixpkgs.lib.licenses.mit; + mainProgram = "capybar"; + }; }; # Function to build dev shell @@ -66,23 +71,24 @@ rustfmt clippy pkg-config + fontconfig libxkbcommon - cairo - libpulseaudio ]; - - RUSTFLAGS = "--cfg tokio_unstable"; }; in { # Generate per-system outputs packages = forAllSystems (system: { default = packageFor system; - kapibar = packageFor system; + capybar = packageFor system; }); devShells = forAllSystems (system: { default = devShellFor system; }); + + formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixfmt-tree); + + homeManagerModules.default = import ./nix/module.nix self; }; } diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..aaaea4a --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,26 @@ +self: { + lib, + pkgs, + config, + ... +}: let + cfg = config.programs.capybar; +in with lib; { + options.programs.capybar = { + enable = mkEnableOption "capybar"; + + package = mkOption { + type = types.package; + description = "The capybar package to use."; + default = self.packages.${pkgs.system}.capybar; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + +# xdg.configFile."capybar/config.toml" = { +# text = builtins.toTOML cfg.settings; +# }; + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5f999ce --- /dev/null +++ b/src/main.rs @@ -0,0 +1,92 @@ +use std::{fmt::Display, path::PathBuf}; + +use anyhow::Result; +use capybar::{config::Config, root::Root}; +use clap::{Args, Parser, ValueEnum}; +use std::env::var; +use thiserror::Error; +use wayland_client::{globals::registry_queue_init, Connection}; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct Cli { + #[command(flatten)] + args: Arguments, +} + +#[derive(Debug, Args)] +struct Arguments { + /// What config type to use + #[arg(long, value_enum, default_value_t = ConfigTypes::Toml, value_name = "TYPE")] + cfg_type: ConfigTypes, + + #[arg(long, value_name = "FILE")] + /// Directory where the config is located + cfg_path: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum ConfigTypes { + Toml, +} + +impl Display for ConfigTypes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConfigTypes::Toml => write!(f, "toml"), + } + } +} + +#[derive(Debug, Error)] +enum Errors { + #[error( + "Configuration file does not exist! + Make sure you are passing `--cfg_type ` with correct type if it is not TOML. + Make sure you provide '--cfg_path ' with your config file or \ + place it config at `~/.config/capybar/config." + )] + ConfigNotExist, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + let mut cfg_path; + match cli.args.cfg_path { + None => { + if let Ok(config_home) = var("XDG_CONFIG_HOME") + .or_else(|_| var("HOME").map(|home| format!("{home}/.config"))) + { + cfg_path = config_home.into(); + } else { + return Err(Errors::ConfigNotExist.into()); + } + } + Some(value) => cfg_path = value, + } + + if cfg_path.is_dir() { + cfg_path.push("capybar"); + let file_name = "config.".to_string() + &cli.args.cfg_type.to_string(); + cfg_path.push(file_name); + } + + if !cfg_path.exists() { + return Err(Errors::ConfigNotExist.into()); + } + + let config = match cli.args.cfg_type { + ConfigTypes::Toml => Config::parse_toml(cfg_path)?, + }; + + let conn = Connection::connect_to_env()?; + let (globals, mut event_queue) = registry_queue_init(&conn)?; + + let mut capybar = Root::new(&globals, &mut event_queue)?; + capybar.apply_config(config)?; + + capybar.init(&mut event_queue)?.run(&mut event_queue)?; + + Ok(()) +} diff --git a/src/processes/clients/hyprland/keyboard.rs b/src/processes/clients/hyprland/keyboard.rs index 93558c3..38e3389 100644 --- a/src/processes/clients/hyprland/keyboard.rs +++ b/src/processes/clients/hyprland/keyboard.rs @@ -109,7 +109,7 @@ impl ProcessNew for Keyboard { } } - return Err(anyhow!("No main keyboard found")); + Err(anyhow!("No main keyboard found")) } } diff --git a/src/processes/clients/mod.rs b/src/processes/clients/mod.rs index 9a9e612..d92221b 100644 --- a/src/processes/clients/mod.rs +++ b/src/processes/clients/mod.rs @@ -6,6 +6,7 @@ use super::Process; #[cfg(feature = "hyprland")] pub mod hyprland; +#[allow(dead_code)] #[cfg(feature = "keyboard")] trait KeyboardTrait: Process {} diff --git a/src/processes/mod.rs b/src/processes/mod.rs index 871c56c..ee67a61 100644 --- a/src/processes/mod.rs +++ b/src/processes/mod.rs @@ -14,7 +14,7 @@ use thiserror::Error; use crate::root::Environment; fn default_update_rate() -> i64 { - return 1000; + 1000 } #[derive(Debug, Deserialize, Clone, Copy)] diff --git a/src/util/signals.rs b/src/util/signals.rs index a3f613b..d5dec7a 100644 --- a/src/util/signals.rs +++ b/src/util/signals.rs @@ -20,20 +20,26 @@ type Callback = Box; /// # Examples /// /// ``` -/// let mut signal = Signal::new(); -/// let last_value = Rc::new(RefCell::new(None)); -/// let last_value_clone = Rc::clone(&last_value); +/// use capybar::util::signals::Signal; +/// use std::{cell::RefCell, rc::Rc}; /// -/// signal.connect(move |data| { -/// if let Some(value) = data.downcast_ref::() { -/// *last_value_clone.borrow_mut() = Some(*value); -/// } -/// }); +/// fn main() { +/// let mut signal = Signal::new(); +/// let last_value = Rc::new(RefCell::new(None)); +/// let last_value_clone = Rc::clone(&last_value); /// -/// //... +/// signal.connect(move |data| { +/// if let Some(value) = data.downcast_ref::() { +/// *last_value_clone.borrow_mut() = Some(*value); +/// } +/// }); /// -/// signal.emit(&42i32); +/// //... +/// +/// signal.emit(&42i32); +/// } /// ``` +#[derive(Default)] pub struct Signal { listeners: RefCell>, } diff --git a/src/widgets/keyboard.rs b/src/widgets/keyboard.rs index 4aeb64f..4cb21f9 100644 --- a/src/widgets/keyboard.rs +++ b/src/widgets/keyboard.rs @@ -103,7 +103,7 @@ impl Widget for Keyboard { &text.to_string() }; - signal_text.borrow_mut().change_text(&layout); + signal_text.borrow_mut().change_text(layout); } }); diff --git a/tests/util/signals.rs b/tests/util/signals.rs index c65f6ea..2883b7d 100644 --- a/tests/util/signals.rs +++ b/tests/util/signals.rs @@ -7,7 +7,7 @@ mod tests { #[test] fn basic_usage() { - let mut signal = Signal::new(); + let signal = Signal::new(); let last_value = Rc::new(RefCell::new(None)); let last_value_clone = Rc::clone(&last_value); @@ -28,7 +28,7 @@ mod tests { #[test] fn reacts_to_correct_type() { - let mut signal = Signal::new(); + let signal = Signal::new(); let state = Rc::new(RefCell::new(0)); let state_clone = Rc::clone(&state); @@ -45,7 +45,7 @@ mod tests { #[test] fn ignores_wrong_type() { - let mut signal = Signal::new(); + let signal = Signal::new(); let called = Rc::new(RefCell::new(true)); let called_clone = Rc::clone(&called); @@ -61,7 +61,7 @@ mod tests { #[test] fn latest_emit_applied() { - let mut signal = Signal::new(); + let signal = Signal::new(); let value = Rc::new(RefCell::new(0)); let value_clone = Rc::clone(&value); @@ -86,7 +86,7 @@ mod tests { #[test] fn mixed_types_in_callbacks() { - let mut signal = Signal::new(); + let signal = Signal::new(); let int_state = Rc::new(RefCell::new(0)); let string_state = Rc::new(RefCell::new(String::new())); From 3a5495f642b4c24e59e3b1cdb5245603a9130327 Mon Sep 17 00:00:00 2001 From: pepengu Date: Wed, 16 Jul 2025 15:53:05 +0300 Subject: [PATCH 6/6] style: refactor --- Cargo.lock | 2 +- Cargo.toml | 2 +- nix/module.nix | 4 - src/processes/clients/hyprland/keyboard.rs | 96 ++++++------ src/processes/mod.rs | 18 ++- src/root.rs | 8 +- src/util/signals.rs | 163 +++++++++++++++++---- src/widgets/battery.rs | 21 ++- src/widgets/clock.rs | 10 +- src/widgets/containers/bar.rs | 19 ++- src/widgets/containers/row.rs | 10 +- src/widgets/cpu.rs | 23 +-- src/widgets/keyboard.rs | 23 ++- src/widgets/mod.rs | 14 +- src/widgets/text.rs | 10 +- tests/util/signals.rs | 152 ++++++++++++++++++- 16 files changed, 426 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e5d585c..54d0609 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,7 +239,7 @@ dependencies = [ [[package]] name = "capybar" -version = "0.1.1" +version = "0.2.0" dependencies = [ "anyhow", "battery", diff --git a/Cargo.toml b/Cargo.toml index a7ba6a9..4ce023d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "capybar" description = "Wayland native toolbar" -version = "0.1.1" +version = "0.2.0" edition = "2021" license = "MIT" repository = "https://github.com/CapyCore/capybar" diff --git a/nix/module.nix b/nix/module.nix index aaaea4a..a887707 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -18,9 +18,5 @@ in with lib; { config = mkIf cfg.enable { home.packages = [ cfg.package ]; - -# xdg.configFile."capybar/config.toml" = { -# text = builtins.toTOML cfg.settings; -# }; }; } diff --git a/src/processes/clients/hyprland/keyboard.rs b/src/processes/clients/hyprland/keyboard.rs index 38e3389..2244edb 100644 --- a/src/processes/clients/hyprland/keyboard.rs +++ b/src/processes/clients/hyprland/keyboard.rs @@ -5,7 +5,7 @@ use chrono::{DateTime, Duration, Local}; use hyprland::{data::Devices, shared::HyprData}; use crate::{ - processes::{clients::KeyboardTrait, Process, ProcessNew, ProcessSettings}, + processes::{clients::KeyboardTrait, Process, ProcessError, ProcessNew, ProcessSettings}, root::Environment, util::signals::Signal, }; @@ -20,15 +20,44 @@ pub struct Keyboard { env: Option>, } +impl Keyboard { + fn get_main_keyboard() -> Result { + let devices = Devices::get(); + if let Err(err) = devices { + return Err(ProcessError::Custom("Keyboard".to_string(), err.into())); + } + + let keyboards = devices.unwrap().keyboards; + + if keyboards.is_empty() { + return Err(ProcessError::Custom( + "Keyboard".to_string(), + anyhow!("No Keyboard connected"), + )); + } + + for keyboard in keyboards { + if keyboard.main { + return Ok(keyboard); + } + } + + Err(ProcessError::Custom( + "Keyboard".to_string(), + anyhow!("No main keyboard found"), + )) + } +} + impl Process for Keyboard { - fn bind(&mut self, env: std::rc::Rc) -> anyhow::Result<()> { + fn bind(&mut self, env: std::rc::Rc) -> Result<(), ProcessError> { self.env = Some(Rc::clone(&env)); Ok(()) } - fn init(&self) -> anyhow::Result<()> { + fn init(&self) -> Result<(), ProcessError> { if self.env.is_none() { - return Err(anyhow!("No Environment is bound")); + return Err(ProcessError::RunWithNoEnv("Keyboard".to_string())); } let mut signals = self.env.as_ref().unwrap().signals.borrow_mut(); @@ -36,26 +65,15 @@ impl Process for Keyboard { signals.insert("keyboard".to_string(), Signal::new()); } - let keyboards = Devices::get()?.keyboards; - if keyboards.is_empty() { - return Err(anyhow!("No Keyboard connected")); - } - - let mut last_layout = self.last_layout.borrow_mut(); - for keyboard in keyboards { - if keyboard.main { - *last_layout = keyboard.layout; - } - } - - signals["keyboard"].emit(&last_layout.clone()); + *self.last_layout.borrow_mut() = Keyboard::get_main_keyboard()?.active_keymap; + signals["keyboard"].emit(&self.last_layout.clone()); Ok(()) } - fn run(&self) -> anyhow::Result<()> { + fn run(&self) -> Result<(), ProcessError> { if self.env.is_none() { - return Err(anyhow!("No Environment is bound")); + return Err(ProcessError::RunWithNoEnv("Keyboard".to_string())); } let mut last_update = self.last_update.borrow_mut(); @@ -64,19 +82,12 @@ impl Process for Keyboard { } *last_update = Local::now(); - let keyboards = Devices::get()?.keyboards; - - if keyboards.is_empty() { - return Err(anyhow!("No Keyboard connected")); - } - let signals = self.env.as_ref().unwrap().signals.borrow_mut(); let mut last_layout = self.last_layout.borrow_mut(); - for keyboard in keyboards { - if keyboard.main && *last_layout != keyboard.active_keymap { - *last_layout = keyboard.active_keymap; - signals["keyboard"].emit(&last_layout.clone()); - } + let current_layout = Keyboard::get_main_keyboard()?.active_keymap; + if *last_layout != current_layout { + *last_layout = current_layout; + signals["keyboard"].emit(&last_layout.clone()); } Ok(()) @@ -89,27 +100,16 @@ impl ProcessNew for Keyboard { fn new( env: Option>, settings: Self::Settings, - ) -> anyhow::Result + ) -> Result where Self: Sized, { - let keyboards = Devices::get()?.keyboards; - if keyboards.is_empty() { - return Err(anyhow!("No Keyboard connected")); - } - - for keyboard in keyboards { - if keyboard.main { - return Ok(Keyboard { - settings, - last_update: RefCell::new(DateTime::default()), - last_layout: RefCell::new(String::new()), - env, - }); - } - } - - Err(anyhow!("No main keyboard found")) + Ok(Keyboard { + settings, + last_update: RefCell::new(DateTime::default()), + last_layout: RefCell::new(String::new()), + env, + }) } } diff --git a/src/processes/mod.rs b/src/processes/mod.rs index ee67a61..153b018 100644 --- a/src/processes/mod.rs +++ b/src/processes/mod.rs @@ -7,7 +7,6 @@ pub mod clients; use std::rc::Rc; -use anyhow::Result; use serde::Deserialize; use thiserror::Error; @@ -26,13 +25,13 @@ pub struct ProcessSettings { /// A **data structure** that can be used as a widget inside a capybar. pub trait Process { /// Bind a widget to a new environment. - fn bind(&mut self, env: Rc) -> Result<()>; + fn bind(&mut self, env: Rc) -> Result<(), ProcessError>; /// Prepare `Process` for a first run - fn init(&self) -> Result<()>; + fn init(&self) -> Result<(), ProcessError>; /// Run the process - fn run(&self) -> Result<()>; + fn run(&self) -> Result<(), ProcessError>; } /// A `Process` that can be unifiedly created. @@ -42,10 +41,17 @@ pub trait Process { pub trait ProcessNew { type Settings; - fn new(env: Option>, settings: Self::Settings) -> Result + fn new(env: Option>, settings: Self::Settings) -> Result where Self: Sized; } #[derive(Debug, Error)] -pub enum ProcessError {} +pub enum ProcessError { + /// Argument is a name of a widget + #[error("Trying to run a procces \"{0}\" not bound to any environment")] + RunWithNoEnv(String), + + #[error("Custom error occured in widget \"{0}\": \n \"{1}\"")] + Custom(String, anyhow::Error), +} diff --git a/src/root.rs b/src/root.rs index c47b5f5..e305daf 100644 --- a/src/root.rs +++ b/src/root.rs @@ -39,7 +39,7 @@ use wayland_client::{ use crate::{ config::Config, - processes::{clients, Process, ProcessNew}, + processes::{clients, Process, ProcessError, ProcessNew}, util::{ fonts::{self, FontsError}, signals::Signal, @@ -47,7 +47,7 @@ use crate::{ }, widgets::{ self, battery::Battery, clock::Clock, containers::bar::Bar, cpu::CPU, keyboard::Keyboard, - text::Text, Widget, WidgetNew, WidgetsList, + text::Text, Widget, WidgetError, WidgetNew, WidgetsList, }, }; @@ -518,7 +518,7 @@ impl Root { pub fn create_widget(&mut self, f: F, settings: W::Settings) -> Result<()> where W: WidgetNew + Widget + 'static, - F: FnOnce(Option>, W::Settings) -> Result, + F: FnOnce(Option>, W::Settings) -> Result, { self.widgets.push(Box::new(f(self.env.clone(), settings)?)); Ok(()) @@ -527,7 +527,7 @@ impl Root { pub fn create_process(&mut self, f: F, settings: W::Settings) -> Result<()> where W: ProcessNew + Process + 'static, - F: FnOnce(Option>, W::Settings) -> Result, + F: FnOnce(Option>, W::Settings) -> Result, { self.processes .push(Box::new(f(self.env.clone(), settings)?)); diff --git a/src/util/signals.rs b/src/util/signals.rs index d5dec7a..bf41de7 100644 --- a/src/util/signals.rs +++ b/src/util/signals.rs @@ -1,66 +1,175 @@ -use std::{any::Any, cell::RefCell}; +use std::{ + any::Any, + cell::{Ref, RefCell}, +}; type Callback = Box; -/// Signal is a mechanism that allows communication between different components +/// Reactive communication channel for decoupled component interaction /// -/// Signal can be treated as a tunnel. You can pass values through the tunnel and -/// have functions that react to those values. This allows to split heavy procceses -/// from displaing the results. This is done by alowing to bind function that can -/// react to a passed data. +/// Signals implement a publish-subscribe pattern where: +/// - Publishers emit values through [emit](Signal::emit) or [emit_unclonable](Signal::emit_unclonable) +/// - Subscribers register callbacks via [connect](Signal::connect) /// -/// Downcasting and validating data is left to user. You are only provided with -/// `dyn Any`. +/// ### Core Features +/// - **Type-erased values**: All emitted values are passed as `&dyn Any` +/// - **Value history**: Optionally stores last emitted value (see [emit](Signal::emit)) +/// - **Immediate callback invocation**: New connections receive current value immediately /// -/// Whenever signal emit is called all the functions connected to a signal are called. +/// ### Behavior Details +/// - **Downcasting responsibility**: Receivers must validate and downcast values +/// - **Callback persistence**: Handlers remain registered until signal destruction /// -/// In capybar signals are stored inside an [Environment](crate::root::Environment) -/// in a `signals` [HashMap](std::collections::HashMap) +/// ### Usage Notes +/// - Prefer `emit` for cloneable types requiring history +/// - Use `emit_unclonable` for non-cloneable types or when history isn't needed +/// - In capybar, signals are stored in an [Environment](crate::root::Environment)'s +/// `signals` [HashMap](std::collections::HashMap) /// /// # Examples -/// /// ``` /// use capybar::util::signals::Signal; /// use std::{cell::RefCell, rc::Rc}; /// -/// fn main() { -/// let mut signal = Signal::new(); -/// let last_value = Rc::new(RefCell::new(None)); -/// let last_value_clone = Rc::clone(&last_value); -/// -/// signal.connect(move |data| { -/// if let Some(value) = data.downcast_ref::() { -/// *last_value_clone.borrow_mut() = Some(*value); -/// } -/// }); +/// let signal = Signal::new(); +/// let tracker = Rc::new(RefCell::new(0)); /// -/// //... +/// // Connect callback that processes i32 values +/// let track = Rc::clone(&tracker); +/// signal.connect(move |data| { +/// if let Some(num) = data.downcast_ref::() { +/// *track.borrow_mut() = *num; +/// } +/// }); /// -/// signal.emit(&42i32); -/// } +/// // Emit value to all connected callbacks +/// signal.emit(&42i32); +/// assert_eq!(*tracker.borrow(), 42); /// ``` #[derive(Default)] pub struct Signal { listeners: RefCell>, + last_value: RefCell>>, } impl Signal { + /// Creates a new, empty Signal instance pub fn new() -> Self { Signal { listeners: RefCell::new(Vec::new()), + last_value: RefCell::new(None), } } - + /// Registers a callback to be invoked on signal emissions + /// + /// The callback will be immediately invoked with the current `last_value` + /// if one exists. All registered callbacks are invoked when [emit](Signal::emit) + /// is called. + /// + /// # Arguments + /// * `callback` - Handler function that receives emitted data as `&dyn Any` + /// + /// Note: Callbacks persist until the Signal is dropped pub fn connect(&self, callback: F) where F: Fn(&dyn Any) + 'static, { + if let Some(value) = &*self.last_value.borrow() { + callback(&**value); + } + self.listeners.borrow_mut().push(Box::new(callback)); } - pub fn emit(&self, value: &T) { + /// Emits a value to all connected callbacks + /// + /// This operation: + /// 1. Clones the value (must implement [Any] + [Clone]) + /// 2. Stores the cloned value as the new `last_value` + /// 3. Invokes all callbacks with a reference to the original value + /// + /// Prefer this over [emit_unclonable](Signal::emit_unclonable) when: + /// - You need value history tracking + /// - Your type is cheap to clone + pub fn emit(&self, value: &T) { + let cloned = (*value).clone(); + *self.last_value.borrow_mut() = Some(Box::new(cloned)); + for callback in &*self.listeners.borrow_mut() { + callback(value); + } + } + + /// Emits a value without storing or cloning it + /// + /// Unlike [emit](Signal::emit): + /// - Doesn't update `last_value` + /// - Doesn't require [Clone] implementation + /// - Slightly more efficient for non-cloneable types + /// + /// Use when: + /// - You don't need value history + /// - The value can't be cloned + /// - Callbacks don't need persistent access to the value + pub fn emit_unclonable(&self, value: &T) { for callback in &*self.listeners.borrow_mut() { callback(value); } } + + /// Returns a read-only reference to the internal last_value storage + /// + /// Example usage: + /// ``` + /// use capybar::util::signals::Signal; + /// + /// let signal = Signal::new(); + /// signal.emit(&42i32); + /// if let Some(value) = &*signal.last_value_ref() { + /// if let Some(num) = value.downcast_ref::() { + /// assert_eq!(*num, 42); + /// } + /// }; + /// ``` + pub fn last_value_ref(&self) -> Ref<'_, Option>> { + self.last_value.borrow() + } + + /// Processes the last value through a callback function + /// + /// Example usage: + /// ``` + /// use capybar::util::signals::Signal; + /// + /// let signal = Signal::new(); + /// signal.emit(&42i32); + /// signal.with_last_value(|any| { + /// if let Some(num) = any.and_then(|a| a.downcast_ref::()) { + /// assert_eq!(*num, 42); + /// } + /// }); + /// ``` + pub fn with_last_value(&self, f: F) -> R + where + F: FnOnce(Option<&dyn Any>) -> R, + { + let last_value = self.last_value_ref(); + let any_ref = last_value.as_ref().map(|boxed| &**boxed as &dyn Any); + f(any_ref) + } + + /// Retrieves a cloned copy of the last value if available and of type T + /// + /// Example usage: + /// ``` + /// use capybar::util::signals::Signal; + /// + /// let signal = Signal::new(); + /// signal.emit(&42i32); + /// if let Some(num) = signal.get_last_value_cloned::() { + /// assert_eq!(num, 42); + /// } + /// ``` + pub fn get_last_value_cloned(&self) -> Option { + self.with_last_value(|opt| opt.and_then(|any| any.downcast_ref::().cloned())) + } } diff --git a/src/widgets/battery.rs b/src/widgets/battery.rs index 692e35b..ca02bb2 100644 --- a/src/widgets/battery.rs +++ b/src/widgets/battery.rs @@ -6,7 +6,7 @@ use serde::Deserialize; use super::{ text::{Text, TextSettings}, - Style, Widget, WidgetData, WidgetNew, WidgetStyled, + Style, Widget, WidgetData, WidgetError, WidgetNew, WidgetStyled, }; const fn battery_not_charging_default() -> [char; 11] { @@ -171,12 +171,15 @@ impl Widget for Battery { &self.data } - fn bind(&mut self, env: std::rc::Rc) -> anyhow::Result<()> { + fn bind( + &mut self, + env: std::rc::Rc, + ) -> anyhow::Result<(), WidgetError> { self.percent.borrow_mut().bind(env.clone())?; self.icon.borrow_mut().bind(env) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { self.icon.borrow_mut().init()?; self.percent.borrow_mut().init()?; @@ -186,7 +189,7 @@ impl Widget for Battery { Ok(()) } - fn draw(&self) -> anyhow::Result<()> { + fn draw(&self) -> anyhow::Result<(), WidgetError> { let info = self.get_info(); let mut prev_charge = self.prev_charge.borrow_mut(); @@ -233,12 +236,18 @@ impl WidgetNew for Battery { fn new( env: Option>, settings: Self::Settings, - ) -> anyhow::Result + ) -> Result where Self: Sized, { + let manager = Manager::new(); + if let Err(err) = manager { + return Err(WidgetError::Custom(err.into())); + } + + let manager = manager.unwrap(); Ok(Self { - manager: Manager::new()?, + manager, icon: RefCell::new(Text::new( env.clone(), diff --git a/src/widgets/clock.rs b/src/widgets/clock.rs index a90215c..ed5ccc9 100644 --- a/src/widgets/clock.rs +++ b/src/widgets/clock.rs @@ -10,7 +10,7 @@ use crate::{ widgets::{text::Text, Widget}, }; -use super::{text::TextSettings, WidgetData, WidgetNew}; +use super::{text::TextSettings, WidgetData, WidgetError, WidgetNew}; fn default_format() -> String { "%H:%M".to_string() @@ -67,11 +67,11 @@ impl Clock { } impl Widget for Clock { - fn bind(&mut self, env: Rc) -> Result<()> { + fn bind(&mut self, env: Rc) -> Result<(), WidgetError> { self.text.borrow_mut().bind(env) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { let text = self.text.borrow_mut(); text.init()?; @@ -85,7 +85,7 @@ impl Widget for Clock { Ok(()) } - fn draw(&self) -> Result<()> { + fn draw(&self) -> Result<(), WidgetError> { self.update(); self.text.borrow_mut().draw() } @@ -98,7 +98,7 @@ impl Widget for Clock { impl WidgetNew for Clock { type Settings = ClockSettings; - fn new(env: Option>, settings: Self::Settings) -> Result + fn new(env: Option>, settings: Self::Settings) -> Result where Self: Sized, { diff --git a/src/widgets/containers/bar.rs b/src/widgets/containers/bar.rs index 55dad65..2e30d41 100644 --- a/src/widgets/containers/bar.rs +++ b/src/widgets/containers/bar.rs @@ -59,7 +59,7 @@ impl Bar { pub fn create_child_left(&mut self, f: F, settings: W::Settings) -> Result<()> where W: WidgetNew + Widget + 'static, - F: FnOnce(Option>, W::Settings) -> Result, + F: FnOnce(Option>, W::Settings) -> Result, { self.left .borrow_mut() @@ -70,7 +70,7 @@ impl Bar { pub fn create_child_center(&mut self, f: F, settings: W::Settings) -> Result<()> where W: WidgetNew + Widget + 'static, - F: FnOnce(Option>, W::Settings) -> Result, + F: FnOnce(Option>, W::Settings) -> Result, { self.center .borrow_mut() @@ -81,7 +81,7 @@ impl Bar { pub fn create_child_right(&mut self, f: F, settings: W::Settings) -> Result<()> where W: WidgetNew + Widget + 'static, - F: FnOnce(Option>, W::Settings) -> Result, + F: FnOnce(Option>, W::Settings) -> Result, { self.right .borrow_mut() @@ -91,7 +91,10 @@ impl Bar { } impl Widget for Bar { - fn bind(&mut self, env: std::rc::Rc) -> anyhow::Result<()> { + fn bind( + &mut self, + env: std::rc::Rc, + ) -> anyhow::Result<(), WidgetError> { self.left.borrow_mut().bind(Rc::clone(&env))?; self.center.borrow_mut().bind(Rc::clone(&env))?; self.right.borrow_mut().bind(Rc::clone(&env))?; @@ -99,9 +102,9 @@ impl Widget for Bar { Ok(()) } - fn draw(&self) -> anyhow::Result<()> { + fn draw(&self) -> anyhow::Result<(), WidgetError> { if self.env.is_none() { - return Err(WidgetError::DrawWithNoEnv("Bar".to_string()).into()); + return Err(WidgetError::DrawWithNoEnv("Bar".to_string())); } let data = self.data.borrow_mut(); @@ -168,7 +171,7 @@ impl Widget for Bar { Ok(()) } - fn init(&self) -> anyhow::Result<()> { + fn init(&self) -> Result<(), WidgetError> { let left = self.left.borrow_mut(); let center = self.center.borrow_mut(); let right = self.right.borrow_mut(); @@ -206,7 +209,7 @@ impl WidgetNew for Bar { fn new( env: Option>, settings: Self::Settings, - ) -> anyhow::Result + ) -> Result where Self: Sized, { diff --git a/src/widgets/containers/row.rs b/src/widgets/containers/row.rs index a282a22..1578848 100644 --- a/src/widgets/containers/row.rs +++ b/src/widgets/containers/row.rs @@ -60,7 +60,7 @@ pub struct Row { } impl Widget for Row { - fn bind(&mut self, env: Rc) -> Result<()> { + fn bind(&mut self, env: Rc) -> Result<(), WidgetError> { self.env = Some(Rc::clone(&env)); let mut children = self.children.borrow_mut(); @@ -72,7 +72,7 @@ impl Widget for Row { Ok(()) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { let mut data = self.data.borrow_mut(); let mut children = self.children.borrow_mut(); let border = match self.settings.border { @@ -92,9 +92,9 @@ impl Widget for Row { Ok(()) } - fn draw(&self) -> Result<()> { + fn draw(&self) -> Result<(), WidgetError> { if self.env.is_none() { - return Err(WidgetError::DrawWithNoEnv("Row".to_string()).into()); + return Err(WidgetError::DrawWithNoEnv("Row".to_string())); } self.align_children()?; @@ -320,7 +320,7 @@ impl Row { impl WidgetNew for Row { type Settings = RowSettings; - fn new(env: Option>, settings: Self::Settings) -> Result + fn new(env: Option>, settings: Self::Settings) -> Result where Self: Sized, { diff --git a/src/widgets/cpu.rs b/src/widgets/cpu.rs index b74b8f8..944d0b0 100644 --- a/src/widgets/cpu.rs +++ b/src/widgets/cpu.rs @@ -7,7 +7,7 @@ use sysinfo::{CpuRefreshKind, RefreshKind, System}; use super::{ text::{Text, TextSettings}, - Style, Widget, WidgetData, WidgetNew, + Style, Widget, WidgetData, WidgetError, WidgetNew, }; /// Settings of a [CPU] widget @@ -48,7 +48,7 @@ impl CPU { sys.global_cpu_usage().round() as usize } - fn align(&self) -> Result<()> { + fn align(&self) { let icon = self.icon.borrow_mut(); let text = self.percent.borrow_mut(); @@ -73,25 +73,28 @@ impl CPU { + text_data.margin.0 + text_data.margin.1 + text_data.width; - - Ok(()) } } impl Widget for CPU { - fn bind(&mut self, env: std::rc::Rc) -> anyhow::Result<()> { + fn bind( + &mut self, + env: std::rc::Rc, + ) -> anyhow::Result<(), WidgetError> { self.percent.borrow_mut().bind(env.clone())?; self.icon.borrow_mut().bind(env) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { self.icon.borrow_mut().init()?; self.percent.borrow_mut().init()?; - self.align() + self.align(); + + Ok(()) } - fn draw(&self) -> Result<()> { + fn draw(&self) -> Result<(), WidgetError> { let mut last_update = self.last_update.borrow_mut(); if Local::now() - *last_update >= self.update_rate { @@ -107,7 +110,7 @@ impl Widget for CPU { } } - self.align()?; + self.align(); *last_update = Local::now(); } @@ -126,7 +129,7 @@ impl WidgetNew for CPU { fn new( env: Option>, settings: Self::Settings, - ) -> anyhow::Result + ) -> Result where Self: Sized, { diff --git a/src/widgets/keyboard.rs b/src/widgets/keyboard.rs index 4cb21f9..57070b7 100644 --- a/src/widgets/keyboard.rs +++ b/src/widgets/keyboard.rs @@ -40,7 +40,7 @@ pub struct Keyboard { } impl Keyboard { - fn align(&self) -> Result<()> { + fn align(&self) { let icon = self.icon.borrow_mut(); let text = self.text.borrow_mut(); @@ -65,21 +65,19 @@ impl Keyboard { + text_data.margin.0 + text_data.margin.1 + text_data.width; - - Ok(()) } } impl Widget for Keyboard { - fn bind(&mut self, env: Rc) -> Result<()> { + fn bind(&mut self, env: Rc) -> Result<(), WidgetError> { self.env = Some(env.clone()); self.text.borrow_mut().bind(env.clone())?; self.icon.borrow_mut().bind(env) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { if self.env.is_none() { - return Err(WidgetError::InitWithNoEnv("Keyboard".to_string()).into()); + return Err(WidgetError::InitWithNoEnv("Keyboard".to_string())); } let signals = self.env.as_ref().unwrap().signals.borrow_mut(); @@ -88,8 +86,7 @@ impl Widget for Keyboard { return Err(WidgetError::NoCorespondingSignal( "Keyboard".to_string(), "Keyboard".to_string(), - ) - .into()); + )); } let signal_text = Rc::clone(&self.text); @@ -110,11 +107,13 @@ impl Widget for Keyboard { self.icon.borrow_mut().init()?; self.text.borrow_mut().init()?; - self.align() + self.align(); + + Ok(()) } - fn draw(&self) -> Result<()> { - self.align()?; + fn draw(&self) -> Result<(), WidgetError> { + self.align(); self.text.borrow_mut().draw()?; self.icon.borrow_mut().draw() @@ -128,7 +127,7 @@ impl Widget for Keyboard { impl WidgetNew for Keyboard { type Settings = KeyboardSettings; - fn new(env: Option>, settings: Self::Settings) -> Result + fn new(env: Option>, settings: Self::Settings) -> Result where Self: Sized, { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index aedf818..662937f 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -8,7 +8,6 @@ pub mod text; use std::{cell::RefCell, rc::Rc}; -use anyhow::Result; use serde::Deserialize; use thiserror::Error; @@ -19,13 +18,13 @@ use {battery::BatterySettings, clock::ClockSettings, cpu::CPUSettings, text::Tex /// A **data structure** that can be used as a widget inside a capybar. pub trait Widget { /// Bind a widget to a new environment. - fn bind(&mut self, env: Rc) -> Result<()>; + fn bind(&mut self, env: Rc) -> Result<(), WidgetError>; /// Draw an entire widget to a current environment's `Drawer` - fn draw(&self) -> Result<()>; + fn draw(&self) -> Result<(), WidgetError>; /// Prepare `Widget` for a first draw - fn init(&self) -> Result<()>; + fn init(&self) -> Result<(), WidgetError>; /// Return `WidgetData` associated to the widget fn data(&self) -> &RefCell; @@ -38,7 +37,7 @@ pub trait Widget { pub trait WidgetNew: Widget { type Settings; - fn new(env: Option>, settings: Self::Settings) -> Result + fn new(env: Option>, settings: Self::Settings) -> Result where Self: Sized; } @@ -62,6 +61,9 @@ pub enum WidgetError { Maybe process \"{1}\" was not created?" )] NoCorespondingSignal(String, String), + + #[error(transparent)] + Custom(#[from] anyhow::Error), } /// Global common data used by `Widget` data structure. @@ -117,7 +119,7 @@ pub trait WidgetStyled: Widget { fn style_mut(&mut self) -> &mut Style; - fn apply_style(&self) -> Result<()> { + fn apply_style(&self) -> Result<(), WidgetError> { let mut data = self.data().borrow_mut(); let style = self.style(); diff --git a/src/widgets/text.rs b/src/widgets/text.rs index 96a7602..474ea90 100644 --- a/src/widgets/text.rs +++ b/src/widgets/text.rs @@ -91,7 +91,7 @@ impl Text { } impl Widget for Text { - fn bind(&mut self, env: Rc) -> Result<()> { + fn bind(&mut self, env: Rc) -> Result<(), WidgetError> { self.env = Some(env); let _env = self.env.as_mut().unwrap(); @@ -107,16 +107,16 @@ impl Widget for Text { Ok(()) } - fn init(&self) -> Result<()> { + fn init(&self) -> Result<(), WidgetError> { self.update_width(); self.data.borrow_mut().height = self.layout.height() as usize; Ok(()) } - fn draw(&self) -> Result<()> { + fn draw(&self) -> Result<(), WidgetError> { if self.env.is_none() { - return Err(WidgetError::DrawWithNoEnv("Text".to_string()).into()); + return Err(WidgetError::DrawWithNoEnv("Text".to_string())); } let font = &fonts::fonts_vec()[self.settings.fontid]; @@ -146,7 +146,7 @@ impl Widget for Text { impl WidgetNew for Text { type Settings = TextSettings; - fn new(env: Option>, settings: Self::Settings) -> Result + fn new(env: Option>, settings: Self::Settings) -> Result where Self: Sized, { diff --git a/tests/util/signals.rs b/tests/util/signals.rs index 2883b7d..124d9b1 100644 --- a/tests/util/signals.rs +++ b/tests/util/signals.rs @@ -1,10 +1,54 @@ #[cfg(test)] mod tests { - use std::cell::RefCell; + use std::cell::{Cell, RefCell}; use std::rc::Rc; use capybar::util::signals::Signal; + #[test] + fn initial_state() { + let signal = Signal::new(); + assert!(signal.last_value_ref().is_none()); + } + + #[test] + fn emit_store_last_value() { + let signal = Signal::new(); + signal.emit(&42i32); + assert_eq!(signal.get_last_value_cloned::(), Some(42)); + } + + #[test] + fn emit_unclonable_does_not_store() { + let signal = Signal::new(); + signal.emit(&100u8); + signal.emit_unclonable(&200u8); + assert_eq!(signal.get_last_value_cloned::(), Some(100)); + } + #[test] + + fn connect_triggers_immediately() { + let signal = Signal::new(); + signal.emit(&"initial"); + let triggered = Rc::new(Cell::new(false)); + let trigger_clone = Rc::clone(&triggered); + signal.connect(move |data| { + if data.downcast_ref::<&str>().is_some() { + trigger_clone.set(true); + } + }); + assert!(triggered.get()); + } + + #[test] + fn no_immediate_trigger_without_last_value() { + let signal = Signal::new(); + let triggered = Rc::new(Cell::new(false)); + let trigger_clone = Rc::clone(&triggered); + signal.connect(move |_| trigger_clone.set(true)); + assert!(!triggered.get()); + } + #[test] fn basic_usage() { let signal = Signal::new(); @@ -24,6 +68,71 @@ mod tests { signal.emit(&100i32); assert_eq!(*last_value.borrow(), Some(100)); + + signal.emit_unclonable(&42i32); + assert_eq!(*last_value.borrow(), Some(42)); + + signal.emit_unclonable(&100i32); + assert_eq!(*last_value.borrow(), Some(100)); + } + + #[test] + #[should_panic(expected = "already borrowed")] + fn recursive_emit_panics() { + let signal = Signal::new(); + let signal_clone = Rc::new(signal); + let weak = Rc::downgrade(&signal_clone); + signal_clone.connect(move |_| { + if let Some(s) = weak.upgrade() { + s.emit(&"recursive"); + } + }); + signal_clone.emit(&"trigger"); + } + + #[test] + fn type_erasure_handling() { + let signal = Signal::new(); + signal.emit(&1i32); + signal.emit(&"string"); + let result = signal.get_last_value_cloned::<&str>(); + assert_eq!(result, Some("string")); + } + + #[test] + fn with_last_value_helper() { + let signal = Signal::new(); + signal.emit(&999u64); + signal.with_last_value(|opt| { + assert_eq!( + opt.and_then(|v| v.downcast_ref::()).copied(), + Some(999) + ); + }); + } + + #[test] + fn non_clone_type_emission() { + struct NonClone(i32); + let signal = Signal::new(); + signal.emit_unclonable(&NonClone(42)); + // Verify emission occurred via callback + let received = Rc::new(Cell::new(None)); + let recv_clone = Rc::clone(&received); + signal.connect(move |data| { + if let Some(nc) = data.downcast_ref::() { + recv_clone.set(Some(nc.0)); + } + }); + signal.emit_unclonable(&NonClone(100)); + assert_eq!(received.get(), Some(100)); + } + + #[test] + fn get_last_value_wrong_type() { + let signal = Signal::new(); + signal.emit(&5i16); + assert!(signal.get_last_value_cloned::().is_none()); } #[test] @@ -41,6 +150,10 @@ mod tests { signal.emit(&42i32); signal.emit(&"ignore"); assert_eq!(*state.borrow(), 42); + + signal.emit_unclonable(&42i32); + signal.emit_unclonable(&"ignore"); + assert_eq!(*state.borrow(), 84); } #[test] @@ -56,6 +169,7 @@ mod tests { }); signal.emit(&"not a bool"); + signal.emit_unclonable(&"not a bool"); assert!(*called.borrow()); } @@ -76,12 +190,41 @@ mod tests { signal.emit(&3i32); assert_eq!(*value.borrow(), 3); + + signal.emit_unclonable(&1i32); + signal.emit_unclonable(&2i32); + signal.emit_unclonable(&3i32); + + assert_eq!(*value.borrow(), 3); } #[test] fn no_panic_on_no_listeners() { let signal = Signal::new(); signal.emit(&"test"); + signal.emit_unclonable(&42i32); + } + + #[test] + fn emit_many_listeners() { + let signal = Signal::new(); + let count = Rc::new(RefCell::new(0)); + + // Add 1000 listeners + for _ in 0..1000 { + let count_clone = Rc::clone(&count); + signal.connect(move |data| { + if data.downcast_ref::().is_some() { + *count_clone.borrow_mut() += 1; + } + }); + } + + signal.emit(&1); + assert_eq!(*count.borrow(), 1000); + + signal.emit_unclonable(&1); + assert_eq!(*count.borrow(), 2000); } #[test] @@ -110,5 +253,12 @@ mod tests { assert_eq!(*int_state.borrow(), 30); assert_eq!(*string_state.borrow(), "text"); + + signal.emit_unclonable(&10); + signal.emit_unclonable(&"test"); + signal.emit_unclonable(&20); + + assert_eq!(*int_state.borrow(), 60); + assert_eq!(*string_state.borrow(), "test"); } }