From 9fde2849c49a6716c4a8024f69262752d535b9dc Mon Sep 17 00:00:00 2001 From: Viktor Bezuglov Date: Tue, 3 Sep 2024 23:27:02 +0600 Subject: [PATCH] feat(config): add support for configurable parameters in RON format - Bumped version from 0.1.10-alpha to 0.1.11-alpha in Cargo.toml - Added new dependency `ron = "0.8"` to Cargo.toml - Introduced a new configuration file format (`config.ron`) with various parameters like `expire_timeout`, `icon_size`, `log_level`, and more - Created a `Config` struct to manage application settings and implement defaults - Replaced various hard-coded configuration values with dynamic values from the new configuration - Updated README.md to include information on the new configuration file - Made GUI components configurable based on user-defined settings in `config.ron` --- Cargo.lock | 24 +++- Cargo.toml | 3 +- README.md | 9 +- examples/config/config.ron | 42 +++++++ src/config/mod.rs | 221 +++++++++++++++++++++++++++++++++++++ src/dbus/mod.rs | 4 +- src/gui/utils.rs | 56 ++++++++-- src/gui/window.rs | 73 ++++++------ src/main.rs | 20 +--- src/types.rs | 21 +--- src/utils.rs | 1 - 11 files changed, 391 insertions(+), 83 deletions(-) create mode 100644 examples/config/config.ron create mode 100644 src/config/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 7012bb0..3565b98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -216,11 +216,20 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -1184,6 +1193,18 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64", + "bitflags", + "serde", + "serde_derive", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1208,13 +1229,14 @@ dependencies = [ [[package]] name = "rustyfications" -version = "0.1.10-alpha" +version = "0.1.11-alpha" dependencies = [ "env_logger", "futures", "gtk4", "gtk4-layer-shell", "log", + "ron", "serde", "systemd-journal-logger", "time", diff --git a/Cargo.toml b/Cargo.toml index e9bbc05..b38ee54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustyfications" -version = "0.1.10-alpha" +version = "0.1.11-alpha" edition = "2021" authors = ["bzglve"] @@ -16,6 +16,7 @@ sys_logger = { version = "2.1", package = "systemd-journal-logger" } # common futures = "0.3" +ron = "0.8" serde = { version = "1.0", features = ["derive"] } time = { version = "0.3", features = ["local-offset"] } zbus = "4.4" diff --git a/README.md b/README.md index f9b91f3..e326382 100644 --- a/README.md +++ b/README.md @@ -56,12 +56,17 @@ sudo cp target/release/rustyfications /usr/bin/rustyfications From now you don't need to manually start daemon. It will be activated automatically on any client request +## Configuration + +Default configuration provided in example [config.ron](examples/config/config.ron). It should be placed in user config dir either systems ( `~/.config/rustyfications/config.ron` / `/etc/xdg/rustyfications/config.ron` ) + ## Planning - configuration - more capabilities and hints support +- styling customization - ipc like stuff to call already running daemon -- [SwayNotificationCcenter](https://github.com/ErikReider/SwayNotificationCenter)-like sidebar window +- [SwayNotificationCenter](https://github.com/ErikReider/SwayNotificationCenter)-like sidebar window - packaging ## Motivation @@ -70,4 +75,4 @@ Practice myself and write something that I will use every day. Other tools don't quite meet my needs. -Highly inspired by [SwayNotificationCcenter](https://github.com/ErikReider/SwayNotificationCenter) +Highly inspired by [SwayNotificationCenter](https://github.com/ErikReider/SwayNotificationCenter) diff --git a/examples/config/config.ron b/examples/config/config.ron new file mode 100644 index 0000000..5097dec --- /dev/null +++ b/examples/config/config.ron @@ -0,0 +1,42 @@ +( + // we are respect freedesktop timeouts + // this parameter is for notification with undefined expiration + expire_timeout: 5000, + + icon_size: 72, + + // Off, Error, Warn, Info, Debug, Trace + log_level: Info, + + show_app_name: false, + + // that icon is used in upper right corner + window_close_icon: "window-close", + + // some action keys can not have matching icon names + // that map used as an alias for buttons icons + icon_redefines: { + "inline-reply": "mail-reply", + "dismiss": "window-close", + }, + + // most recent notifications appears from anchored edge + new_on_top: true, + + // (width, height) + // (410, 30) - optimal size for display 40 characters in 12px font with 5px window "padding" + window_size: (410, 30), + + // window anchors (position) + // Left, Right, Top, Bottom + // using two opposite anchors to stretch window(s) is not allowed + edges: [Top, Right], + + // margins is used for spacing between windows + // the order is just like in `edges` field + margins: [5, 5], + + // paddings is used for spacing all notifications from the edges + // the order is just like in `edges` field + paddings: [5, 0], +) diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..71f2be5 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,221 @@ +use std::{ + collections::HashMap, + fs, + sync::{LazyLock, Mutex}, +}; + +use defaults::*; +use edge::Edge; +use gtk::glib; +use level_filter::LevelFilter; +use serde::{Deserialize, Serialize}; + +pub mod level_filter { + use super::*; + + use log::LevelFilter as LogLevelFilter; + + #[derive(Debug, Deserialize, Serialize, Clone, Copy)] + pub enum LevelFilter { + Off, + Error, + Warn, + Info, + Debug, + Trace, + } + + impl Default for LevelFilter { + fn default() -> Self { + Self::Info + } + } + + impl From for LogLevelFilter { + fn from(value: LevelFilter) -> Self { + match value { + LevelFilter::Off => Self::Off, + LevelFilter::Error => Self::Error, + LevelFilter::Warn => Self::Warn, + LevelFilter::Info => Self::Info, + LevelFilter::Debug => Self::Debug, + LevelFilter::Trace => Self::Trace, + } + } + } +} + +pub mod edge { + use serde::{Deserialize, Serialize}; + + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] + pub enum Edge { + Left, + Right, + Top, + Bottom, + } + + impl From for gtk_layer_shell::Edge { + fn from(value: Edge) -> Self { + match value { + Edge::Left => gtk_layer_shell::Edge::Left, + Edge::Right => gtk_layer_shell::Edge::Right, + Edge::Top => gtk_layer_shell::Edge::Top, + Edge::Bottom => gtk_layer_shell::Edge::Bottom, + } + } + } +} + +mod defaults { + use std::collections::HashMap; + + use super::{edge::Edge, level_filter::LevelFilter}; + + pub fn expire_timeout() -> u64 { + 5000 + } + + pub fn new_on_top() -> bool { + true + } + + pub fn icon_size() -> i32 { + 72 + } + + pub fn log_level() -> LevelFilter { + LevelFilter::Info + } + + pub fn window_close_icon() -> String { + "window-close".to_owned() + } + + pub fn show_app_name() -> bool { + false + } + + pub fn window_size() -> (i32, i32) { + (410, 30) + } + + pub fn icon_redefines() -> HashMap { + HashMap::new() + } + + pub fn edges() -> Vec { + vec![Edge::Top, Edge::Right] + } + + pub fn margins() -> Vec { + vec![5, 5] + } + + pub fn paddings() -> Vec { + vec![5, 0] + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Config { + #[serde(default = "expire_timeout")] + pub expire_timeout: u64, + + #[serde(default = "new_on_top")] + pub new_on_top: bool, + + #[serde(default = "icon_size")] + pub icon_size: i32, + + #[serde(default = "log_level")] + pub log_level: LevelFilter, + + #[serde(default = "window_close_icon")] + pub window_close_icon: String, + + #[serde(default = "show_app_name")] + pub show_app_name: bool, + + #[serde(default = "window_size")] + pub window_size: (i32, i32), + + #[serde(default = "icon_redefines")] + pub icon_redefines: HashMap, + + #[serde(default = "edges")] + pub edges: Vec, + + #[serde(default = "margins")] + pub margins: Vec, + + #[serde(default = "paddings")] + pub paddings: Vec, +} + +impl Config { + pub fn new() -> Option { + let path; + + let user_config = glib::user_config_dir() + .join("rustyfications") + .join("config.ron"); + + if user_config.exists() { + path = user_config; + } else if let Some(system_config) = glib::system_config_dirs().first() { + let system_config = system_config + .to_path_buf() + .join("rustyfications") + .join("config.ron"); + if system_config.exists() { + path = system_config; + } else { + return None; + } + } else { + return None; + } + + println!("Found config file {:?}", path); + match ron::from_str::(&fs::read_to_string(path.clone()).unwrap()) { + Ok(r) => { + if r.edges.contains(&Edge::Left) && r.edges.contains(&Edge::Right) + || r.edges.contains(&Edge::Top) && r.edges.contains(&Edge::Bottom) + { + eprintln!("Using two opposite edges is not allowed"); + println!("Using default configuration"); + return None; + } + Some(r) + } + Err(e) => { + eprintln!("{}", e); + println!("Using default configuration"); + None + } + } + } +} + +impl Default for Config { + fn default() -> Self { + Self { + expire_timeout: expire_timeout(), + new_on_top: new_on_top(), + icon_size: icon_size(), + log_level: log_level(), + window_close_icon: window_close_icon(), + show_app_name: show_app_name(), + window_size: window_size(), + icon_redefines: icon_redefines(), + edges: edges(), + margins: margins(), + paddings: paddings(), + } + } +} + +pub static CONFIG: LazyLock> = + LazyLock::new(|| Mutex::new(Config::new().unwrap_or_default())); diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index aadebb6..a6040c4 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -20,7 +20,7 @@ use zbus::{ zvariant::{OwnedValue as Value, Type}, }; -use crate::DEFAULT_EXPIRE_TIMEOUT; +use crate::config::CONFIG; static BUS_NAME: &str = "org.freedesktop.Notifications"; static BUS_OBJECT_PATH: &str = "/org/freedesktop/Notifications"; @@ -144,7 +144,7 @@ impl IFace { .collect(), hints, expire_timeout: match expire_timeout.cmp(&0) { - Ordering::Less => DEFAULT_EXPIRE_TIMEOUT, + Ordering::Less => Duration::from_millis(CONFIG.lock().unwrap().expire_timeout), Ordering::Equal => Duration::MAX, Ordering::Greater => Duration::from_millis(expire_timeout as u64), }, diff --git a/src/gui/utils.rs b/src/gui/utils.rs index f1c394c..b0c8a96 100644 --- a/src/gui/utils.rs +++ b/src/gui/utils.rs @@ -1,33 +1,69 @@ use gtk::prelude::WidgetExt; use gtk_layer_shell::{Edge, LayerShell}; -use crate::{types::RuntimeData, NEW_ON_TOP}; +use crate::{ + config::{edge::Edge as ConfigEdge, CONFIG}, + types::RuntimeData, +}; use super::window::Window; pub fn init_layer_shell(window: &impl LayerShell) { window.init_layer_shell(); - window.set_anchor(Edge::Top, true); - window.set_anchor(Edge::Right, true); + let edges = CONFIG.lock().unwrap().edges.clone(); + let margins = CONFIG.lock().unwrap().margins.clone(); + let paddings = CONFIG.lock().unwrap().paddings.clone(); - window.set_margin(Edge::Right, 5); + window.set_anchor(Edge::Left, edges.contains(&ConfigEdge::Left)); + window.set_anchor(Edge::Right, edges.contains(&ConfigEdge::Right)); + window.set_anchor(Edge::Top, edges.contains(&ConfigEdge::Top)); + window.set_anchor(Edge::Bottom, edges.contains(&ConfigEdge::Bottom)); + + for (edge, margin) in edges + .iter() + .zip(margins.iter().zip(paddings.iter()).map(|(m, p)| m + p)) + { + window.set_margin((*edge).into(), margin); + } } pub fn margins_update(runtime_data: RuntimeData) { + let edges = CONFIG.lock().unwrap().edges.clone(); + let margins = CONFIG.lock().unwrap().margins.clone(); + let paddings = CONFIG.lock().unwrap().paddings.clone(); + let runtime_data = runtime_data.borrow(); let windows = runtime_data.windows.iter(); - let iter: Box> = if NEW_ON_TOP { + let new_on_top = CONFIG.lock().unwrap().new_on_top; + let iter: Box> = if new_on_top { Box::new(windows.rev()) } else { Box::new(windows) }; - let mut indent = 5; + let mut indent = paddings + .iter() + .zip(edges.iter()) + .find(|(_, e)| **e == ConfigEdge::Top || **e == ConfigEdge::Bottom) + .map(|(p, _)| p) + .cloned() + .unwrap_or_default(); for (_, window) in iter { - window.inner.set_margin(Edge::Top, indent); - indent += window.inner.height() + 5; + if edges.contains(&ConfigEdge::Top) { + window.inner.set_margin(Edge::Top, indent); + } else if edges.contains(&ConfigEdge::Bottom) { + window.inner.set_margin(Edge::Bottom, indent); + } + indent += window.inner.height() + + margins + .iter() + .zip(edges.iter()) + .find(|(_, e)| **e == ConfigEdge::Left || **e == ConfigEdge::Right) + .map(|(p, _)| p) + .cloned() + .unwrap_or_default(); } } @@ -36,7 +72,7 @@ pub mod pixbuf { use gtk::{gdk, gdk_pixbuf::Pixbuf, prelude::FileExt, IconLookupFlags, TextDirection}; - use crate::ICON_SIZE; + use crate::config::CONFIG; pub fn new_from_str(value: &str) -> Option { if PathBuf::from(value).is_absolute() { @@ -47,7 +83,7 @@ pub mod pixbuf { let ipaint = itheme.lookup_icon( value, &["image-missing"], - ICON_SIZE, + CONFIG.lock().unwrap().icon_size, 1, TextDirection::None, IconLookupFlags::empty(), diff --git a/src/gui/window.rs b/src/gui/window.rs index 4d09aec..d22d190 100644 --- a/src/gui/window.rs +++ b/src/gui/window.rs @@ -12,9 +12,9 @@ use gtk::{ use log::*; use crate::{ + config::CONFIG, dbus::{Details, IFace, IFaceRef, Reason}, types::RuntimeData, - ICON_SIZE, WINDOW_CLOSE_ICON, }; use super::utils::{init_layer_shell, pixbuf}; @@ -22,7 +22,7 @@ use super::utils::{init_layer_shell, pixbuf}; #[derive(Clone)] pub struct Window { pub id: u32, - // app_name: gtk::Label, + app_name: gtk::Label, icon: gtk::Image, summary: gtk::Label, app_icon: gtk::Image, @@ -74,7 +74,7 @@ impl Window { runtime_data: RuntimeData, ) -> Self { info!("Building window from details: {:?}", details); - let window = Window::from_details(details.clone(), iface.clone(), runtime_data.clone()); + let window = Window::from_details(details.clone(), iface.clone()); init_layer_shell(&window.inner); window.inner.set_application(Some(&application)); @@ -120,12 +120,7 @@ impl Window { window } - pub fn update_from_details( - &mut self, - value: &Details, - iface: Rc, - runtime_data: RuntimeData, - ) { + pub fn update_from_details(&mut self, value: &Details, iface: Rc) { debug!("Updating window from details: {:?}", value); if self.thandle.borrow().is_some() { self.stop_timeout(); @@ -133,10 +128,10 @@ impl Window { let value = value.clone(); - // // TODO visibility of app name should depend on configuration - // self.app_name - // .set_label(&value.app_name.clone().unwrap_or_default()); - // self.app_name.set_visible(value.app_name.is_some()); + self.app_name + .set_label(&value.app_name.clone().unwrap_or_default()); + self.app_name + .set_visible(CONFIG.lock().unwrap().show_app_name); self.summary.set_label(&value.summary); @@ -178,7 +173,8 @@ impl Window { } } } else { - self.app_icon.set_icon_name(Some(WINDOW_CLOSE_ICON)); + self.app_icon + .set_icon_name(Some(&CONFIG.lock().unwrap().window_close_icon.clone())); } self.body.set_label(&value.body.clone().unwrap_or_default()); @@ -203,8 +199,8 @@ impl Window { if !value.hints.action_icons { btn.set_label(&action.text); } else { - let runtime_data = runtime_data.borrow(); - let redef = runtime_data.icon_redefines.get(&action.key).unwrap_or(&action.key); + let config = CONFIG.lock().unwrap().clone(); + let redef = config.icon_redefines.get(&action.key).unwrap_or(&action.key); btn.set_icon_name(redef); } btn.set_tooltip_text(Some(&action.text)); @@ -277,20 +273,27 @@ impl Window { fn build_widgets_tree(value: &Details) -> Self { trace!("Building widget tree for window id: {}", value.id); + let config = CONFIG.lock().unwrap().clone(); + let inner = gtk::Window::builder() - // optimal size to display 40 chars in 12px font and 5px margin - .default_width(410) - .default_height(30) + .default_width(config.window_size.0) + .default_height(config.window_size.1) .name("notification") .build(); - // let app_name = gtk::Label::builder() - // .name("app_name") - // .justify(Justification::Left) - // .halign(Align::Start) - // .ellipsize(EllipsizeMode::End) - // .sensitive(false) - // .build(); + let app_name = gtk::Label::builder() + .name("app_name") + .justify(Justification::Left) + .halign(Align::Start) + .ellipsize(EllipsizeMode::End) + .sensitive(false) + .build(); + let app_name_box = gtk::Box::builder() + .orientation(Orientation::Horizontal) + .spacing(5) + .visible(CONFIG.lock().unwrap().show_app_name) + .build(); + app_name_box.append(&app_name); let summary_box = gtk::Box::builder() .orientation(Orientation::Horizontal) @@ -339,7 +342,7 @@ impl Window { } } - app_icon.set_icon_name(Some(WINDOW_CLOSE_ICON)); + app_icon.set_icon_name(Some(&CONFIG.lock().unwrap().window_close_icon.clone())); } )); @@ -367,7 +370,11 @@ impl Window { )); summary_box.append(&summary); - summary_box.append(&app_icon); + if CONFIG.lock().unwrap().show_app_name { + app_name_box.append(&app_icon); + } else { + summary_box.append(&app_icon); + } let body = gtk::Label::builder() .name("body") @@ -389,7 +396,7 @@ impl Window { .valign(Align::Start) .spacing(5) .build(); - // main_box.append(&app_name); + main_box.append(&app_name_box); main_box.append(&summary_box); main_box.append(&body); main_box.append(&reply_revealer); @@ -400,7 +407,7 @@ impl Window { .build(); let icon = gtk::Image::builder() .visible(false) - .pixel_size(ICON_SIZE) + .pixel_size(CONFIG.lock().unwrap().icon_size) .valign(Align::Center) .halign(Align::End) .build(); @@ -424,7 +431,7 @@ impl Window { trace!("Widget tree built for window id: {}", value.id); Self { id: value.id, - // app_name, + app_name, icon, summary, app_icon, @@ -438,7 +445,7 @@ impl Window { } } - pub fn from_details(value: Details, iface: Rc, runtime_data: RuntimeData) -> Self { + pub fn from_details(value: Details, iface: Rc) -> Self { info!("Creating window from details for id: {}", value.id); let mut _self = Self::build_widgets_tree(&value); @@ -454,7 +461,7 @@ impl Window { } )); - _self.update_from_details(&value, iface, runtime_data); + _self.update_from_details(&value, iface); _self } diff --git a/src/main.rs b/src/main.rs index 0502b26..8d99600 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod config; mod dbus; mod gui; mod types; @@ -5,6 +6,7 @@ mod utils; use std::{error::Error, rc::Rc, sync::Arc, time::Duration}; +use config::CONFIG; use dbus::{Action, Details, IFace, IFaceRef, Message, Reason, ServerInfo}; use futures::{channel::mpsc, lock::Mutex, StreamExt}; use gtk::{ @@ -21,14 +23,6 @@ use utils::{close_hook, load_css}; pub static MAIN_APP_ID: &str = "com.bzglve.rustyfications"; -// TODO move to config -pub static DEFAULT_EXPIRE_TIMEOUT: Duration = Duration::from_secs(5); -pub static NEW_ON_TOP: bool = true; -pub static ICON_SIZE: i32 = 72; -pub static LOG_LEVEL: LevelFilter = LevelFilter::Trace; - -pub static WINDOW_CLOSE_ICON: &str = "window-close"; - fn main() -> Result<(), Box> { if connected_to_journal() { JournalLog::new() @@ -39,7 +33,7 @@ fn main() -> Result<(), Box> { } else { env_logger::init(); } - log::set_max_level(LOG_LEVEL); + log::set_max_level(CONFIG.lock().unwrap().log_level.into()); info!("Starting application..."); @@ -103,11 +97,7 @@ fn main() -> Result<(), Box> { "Updating existing notification window with id: {}", details.id ); - window.update_from_details( - &details, - iface.clone(), - runtime_data.clone(), - ); + window.update_from_details(&details, iface.clone()); window.start_timeout(); } @@ -137,6 +127,8 @@ fn main() -> Result<(), Box> { )); load_css(); + + debug!("CONFIG: {:#?}", CONFIG.lock().unwrap()); }); application.connect_activate(build_ui); diff --git a/src/types.rs b/src/types.rs index 362cf09..48224fe 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,27 +1,10 @@ -use std::{ - cell::RefCell, - collections::{BTreeMap, HashMap}, - rc::Rc, -}; +use std::{cell::RefCell, collections::BTreeMap, rc::Rc}; use crate::gui::window::Window; pub type RuntimeData = Rc>; +#[derive(Default)] pub struct _RuntimeData { pub windows: BTreeMap, - pub icon_redefines: HashMap, // TODO it has to be in config -} - -impl Default for _RuntimeData { - fn default() -> Self { - let mut icon_redefines = HashMap::new(); - icon_redefines.insert("inline-reply".to_owned(), "mail-reply".to_owned()); - icon_redefines.insert("dismiss".to_owned(), "window-close".to_owned()); - - Self { - windows: Default::default(), - icon_redefines, - } - } } diff --git a/src/utils.rs b/src/utils.rs index b9a2145..952b076 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -30,7 +30,6 @@ mod css { borders, theme_base_color ); - // TODO move to config? provider.load_from_data(&format!( " #notification {{