From 77627207d888b58d81992baf40560c8d2c51a9e0 Mon Sep 17 00:00:00 2001 From: candifloss Date: Thu, 11 Dec 2025 13:35:29 +0530 Subject: [PATCH] Configuration crate - Write config library crate - Add comments and default values - Update UI binary to add config loading --- crates/popcorn-conf/src/icon.rs | 46 ++++++++ crates/popcorn-conf/src/lib.rs | 71 ++++++++++-- crates/popcorn-conf/src/percent_value.rs | 38 +++++++ crates/popcorn-conf/src/ui.rs | 63 ++++++++++ crates/popcorn-conf/src/window.rs | 38 +++++++ crates/popcorn/src/show_popup.rs | 139 ++++++++++------------- 6 files changed, 307 insertions(+), 88 deletions(-) create mode 100644 crates/popcorn-conf/src/icon.rs create mode 100644 crates/popcorn-conf/src/percent_value.rs create mode 100644 crates/popcorn-conf/src/ui.rs create mode 100644 crates/popcorn-conf/src/window.rs diff --git a/crates/popcorn-conf/src/icon.rs b/crates/popcorn-conf/src/icon.rs new file mode 100644 index 0000000..71e4727 --- /dev/null +++ b/crates/popcorn-conf/src/icon.rs @@ -0,0 +1,46 @@ +use serde::Deserialize; + +/// Icon styling configuration. +/// Corresponds to `[icon]` in the config file. +#[derive(Debug, Deserialize)] +pub struct IconConfig { + /// Icon size in pixels. + #[serde(default = "def_size")] + pub size: u32, + + /// Icon color in AARRGGBB hex format. + #[serde(default = "def_color")] + pub color: String, + + /// Icon font family name. + #[serde(default = "def_font")] + pub font: String, + + /// Default glyph to use when no icon is supplied. + #[serde(default = "def_icon")] + pub default_icon: String, +} + +const fn def_size() -> u32 { + 35 +} +fn def_color() -> String { + "ffffffff".into() +} +fn def_font() -> String { + "IosevkaTermSlab Nerd Font Mono".into() +} +fn def_icon() -> String { + "".into() +} + +impl Default for IconConfig { + fn default() -> Self { + Self { + size: def_size(), + color: def_color(), + font: def_font(), + default_icon: def_icon(), + } + } +} diff --git a/crates/popcorn-conf/src/lib.rs b/crates/popcorn-conf/src/lib.rs index 53f8cad..d94e37f 100644 --- a/crates/popcorn-conf/src/lib.rs +++ b/crates/popcorn-conf/src/lib.rs @@ -1,15 +1,66 @@ -#[must_use] -pub fn add(left: u64, right: u64) -> u64 { - left + right +mod icon; +mod percent_value; +mod ui; +mod window; + +pub use icon::IconConfig; +pub use percent_value::PercentValueConfig; +pub use ui::UiConfig; +pub use window::WindowConfig; + +use dirs::config_dir; +use serde::Deserialize; +use std::path::PathBuf; +use thiserror::Error; + +/// Errors returned by Popcorn configuration loading. +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("I/O error: {0}")] + Io(std::io::Error), + + #[error("TOML parse error: {0}")] + Toml(toml::de::Error), } -#[cfg(test)] -mod tests { - use super::*; +/// Complete Popcorn configuration. +/// Combines window, UI, percent text, and icon styling. +#[derive(Debug, Deserialize, Default)] +pub struct PopcornConfig { + #[serde(default)] + pub window: WindowConfig, - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + #[serde(default)] + pub ui: UiConfig, + + #[serde(default)] + pub percent_value: PercentValueConfig, + + #[serde(default)] + pub icon: IconConfig, +} + +impl PopcornConfig { + /// Load the Popcorn configuration from: + /// `~/.config/candywidgets/popcorn/config.toml` + /// + /// # Errors + /// - `ConfigError::Io` if the file cannot be read + /// - `ConfigError::Toml` if the syntax is invalid + pub fn load() -> Result { + let path = config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("candywidgets/popcorn/config.toml"); + + let text = std::fs::read_to_string(path).map_err(ConfigError::Io)?; + let cfg: PopcornConfig = toml::from_str(&text).map_err(ConfigError::Toml)?; + Ok(cfg) + } + + /// Load configuration.\ + /// If the file is missing or broken, fall back to built-in defaults. + #[must_use] + pub fn load_or_default() -> Self { + Self::load().unwrap_or_default() } } diff --git a/crates/popcorn-conf/src/percent_value.rs b/crates/popcorn-conf/src/percent_value.rs new file mode 100644 index 0000000..cb1da72 --- /dev/null +++ b/crates/popcorn-conf/src/percent_value.rs @@ -0,0 +1,38 @@ +use serde::Deserialize; + +/// Configuration for percentage text styling. +/// Corresponds to `[percent_value]`. +#[derive(Debug, Deserialize)] +pub struct PercentValueConfig { + /// Font size in pixels. + #[serde(default = "def_font_size")] + pub font_size: u32, + + /// Font color in AARRGGBB hex. + #[serde(default = "def_color")] + pub font_color: String, + + /// Font family name. + #[serde(default = "def_font")] + pub font: String, +} + +const fn def_font_size() -> u32 { + 35 +} +fn def_color() -> String { + "ffffffff".into() +} +fn def_font() -> String { + "Iosevka Extrabold".into() +} + +impl Default for PercentValueConfig { + fn default() -> Self { + Self { + font_size: def_font_size(), + font_color: def_color(), + font: def_font(), + } + } +} diff --git a/crates/popcorn-conf/src/ui.rs b/crates/popcorn-conf/src/ui.rs new file mode 100644 index 0000000..0bee1e2 --- /dev/null +++ b/crates/popcorn-conf/src/ui.rs @@ -0,0 +1,63 @@ +use serde::Deserialize; + +/// Configuration for UI geometry and base colors. +/// Corresponds to `[ui]`. +#[derive(Debug, Deserialize)] +pub struct UiConfig { + /// Popup width in pixels. + #[serde(default = "def_width")] + pub width: u32, + + /// Popup height in pixels. + #[serde(default = "def_height")] + pub height: u32, + + /// Corner radius in pixels. + #[serde(default = "def_radius")] + pub border_radius: u32, + + /// Horizontal padding around icon and text. + #[serde(default = "def_padding")] + pub padding: u32, + + /// Background color in AARRGGBB hex format. + #[serde(default = "def_bg")] + pub bg_col: String, + + /// Fallback fill color when none is provided via CLI. + #[serde(default = "def_fill")] + pub default_fill_color: String, +} + +const fn def_width() -> u32 { + 200 +} +const fn def_height() -> u32 { + 50 +} +const fn def_radius() -> u32 { + 5 +} +const fn def_padding() -> u32 { + 7 +} + +fn def_bg() -> String { + "675f5f5f".into() +} +fn def_fill() -> String { + "ff397979".into() +} + +impl Default for UiConfig { + fn default() -> Self { + Self { + width: def_width(), + height: def_height(), + border_radius: def_radius(), + padding: def_padding(), + bg_col: def_bg(), + default_fill_color: def_fill(), + } + } +} diff --git a/crates/popcorn-conf/src/window.rs b/crates/popcorn-conf/src/window.rs new file mode 100644 index 0000000..035b88c --- /dev/null +++ b/crates/popcorn-conf/src/window.rs @@ -0,0 +1,38 @@ +use serde::Deserialize; + +/// Window placement and timeout settings. +/// Corresponds to `[window]`. +#[derive(Debug, Deserialize)] +pub struct WindowConfig { + /// Auto-hide timeout in milliseconds. + #[serde(default = "default_timeout")] + pub popup_timeout: u64, + + /// X position of the popup. + #[serde(default = "default_pos_x")] + pub pos_x: i32, + + /// Y position of the popup. + #[serde(default = "default_pos_y")] + pub pos_y: i32, +} + +const fn default_timeout() -> u64 { + 1300 +} +const fn default_pos_x() -> i32 { + 1046 +} +const fn default_pos_y() -> i32 { + 36 +} + +impl Default for WindowConfig { + fn default() -> Self { + Self { + popup_timeout: default_timeout(), + pos_x: default_pos_x(), + pos_y: default_pos_y(), + } + } +} diff --git a/crates/popcorn/src/show_popup.rs b/crates/popcorn/src/show_popup.rs index 8593923..386aaf8 100644 --- a/crates/popcorn/src/show_popup.rs +++ b/crates/popcorn/src/show_popup.rs @@ -1,4 +1,6 @@ use crate::args::OsdArgs; +use popcorn_conf::PopcornConfig; + use i_slint_backend_winit::{ Backend, winit::{ @@ -9,14 +11,9 @@ use i_slint_backend_winit::{ use slint::{Color, LogicalPosition, LogicalSize, SharedString}; slint::include_modules!(); -// Helper function to simplify color values (A, R, G, B) -fn argb_col(color_hex: u32) -> Color { - Color::from_argb_encoded(color_hex) -} - +/// Convert an AARRGGBB hex string into a Slint Color. fn parse_hex_color(s: &str) -> Result { let clean = s.trim().trim_start_matches('#'); - let raw = u32::from_str_radix(clean, 16).map_err(|_| "invalid hex color")?; if clean.len() == 8 { @@ -26,91 +23,77 @@ fn parse_hex_color(s: &str) -> Result { } } -/// Set Slint properties from config data -fn set_ui_props(ui: &OSDpopup, args: &OsdArgs) -> Result<(), Box> { - // Config values - let popup_width = 200; - let popup_height = 50; - let popup_border_radius = 5; - let popup_padding = 7; - let window_position_x = 1146; - let window_position_y = 35; - - let osd_bg_color = argb_col(0x_67_5f_5f_5f); - let fill_color = if let Some(c) = args.color.as_deref() { - parse_hex_color(c)? - } else { - argb_col(0x_ff_12_7a_9b) - }; - - let value_font_size = 35; - let value_font_color = argb_col(0x_ff_ff_ff_ff); - let value_font = "Iosevka Extrabold"; - let percent_value = i32::from(args.value.unwrap_or(0)); - - let icon_size = value_font_size; - let icon_font_color = value_font_color; - let icon_font = "IosevkaTermSlab Nerd Font Mono"; - let icon_glyph = args.icon.as_deref().unwrap_or(""); - - // Config values -> Slint component properties - ui.set_popup_width(popup_width); - ui.set_popup_height(popup_height); - ui.set_popup_border_radius(popup_border_radius); - ui.set_popup_padding(popup_padding); - - ui.set_osd_bg_color(osd_bg_color); - ui.set_fill_color(fill_color); - - ui.set_value_font_size(value_font_size); - ui.set_value_font_color(value_font_color); - ui.set_value_font(SharedString::from(value_font)); - ui.set_percent_value(percent_value); - - ui.set_icon_size(icon_size); - ui.set_icon_font_color(icon_font_color); - ui.set_icon_font(SharedString::from(icon_font)); - ui.set_icon_glyph(SharedString::from(icon_glyph)); - - // Window size (width, height): pixels - #[allow(clippy::cast_precision_loss)] - ui.window() - .set_size(LogicalSize::new(popup_width as f32, popup_height as f32)); - - // Window position (x,y): pixels - #[allow(clippy::cast_precision_loss)] +/// Apply all UI properties to the Slint component. +/// Uses the config for static styling and CLI args for dynamic values. +#[allow(clippy::cast_precision_loss, clippy::cast_possible_wrap)] +fn set_ui_props( + ui: &OSDpopup, + cfg: &PopcornConfig, + args: &OsdArgs, +) -> Result<(), Box> { + // Window placement from config ui.window().set_position(LogicalPosition::new( - window_position_x as f32, - window_position_y as f32, + cfg.window.pos_x as f32, + cfg.window.pos_y as f32, )); + // Window size from config + ui.window() + .set_size(LogicalSize::new(cfg.ui.width as f32, cfg.ui.height as f32)); + + // Container geometry + ui.set_popup_width(cfg.ui.width as i32); + ui.set_popup_height(cfg.ui.height as i32); + ui.set_popup_border_radius(cfg.ui.border_radius as i32); + ui.set_popup_padding(cfg.ui.padding as i32); + + // Background color + ui.set_osd_bg_color(parse_hex_color(&cfg.ui.bg_col)?); + + // Fill color: CLI overrides config fallback + let fill_color_hex = args.color.as_deref().unwrap_or(&cfg.ui.default_fill_color); + ui.set_fill_color(parse_hex_color(fill_color_hex)?); + + // Percentage value styling + ui.set_value_font_size(cfg.percent_value.font_size as i32); + ui.set_value_font_color(parse_hex_color(&cfg.percent_value.font_color)?); + ui.set_value_font(SharedString::from(cfg.percent_value.font.as_str())); + ui.set_percent_value(i32::from(args.value.unwrap_or(0))); + + // Icon styling + ui.set_icon_size(cfg.icon.size as i32); + ui.set_icon_font_color(parse_hex_color(&cfg.icon.color)?); + ui.set_icon_font(SharedString::from(cfg.icon.font.as_str())); + + // Icon glyph: CLI overrides config fallback + let icon_glyph = args + .icon + .as_deref() + .unwrap_or(cfg.icon.default_icon.as_str()); + + ui.set_icon_glyph(SharedString::from(icon_glyph)); + Ok(()) } -/// Create and show the Window UI +/// Create and display the OSD popup window. +/// Loads config, applies X11 window attributes, sets UI props, and runs the Slint event loop. pub fn show_popup(args: &OsdArgs) -> Result<(), Box> { - // Closure that adjusts winit WindowAttributes before Slint creates the window. - let window_attrs = |attrs: WindowAttributes| { - // Mark the X11 window as a Dock so the WM doesn't treat it as a normal window. Window type `Notification` didn't work. Fix later. - attrs.with_x11_window_type(vec![WindowType::Dock]) - }; + let cfg = PopcornConfig::load_or_default(); - // Build a Slint backend that applies this attribute hook to all windows. + // Mark the window as a dock-type override window so WMs treat it like an OSD layer. + let window_attrs = |attrs: WindowAttributes| attrs.with_x11_window_type(vec![WindowType::Dock]); + + // Build and activate backend with X11 window-attribute hook. let backend = Backend::builder() - .with_window_attributes_hook(window_attrs) // Register the hook - .build()?; // Construct backend - - // Activate this customized backend for all Slint window creation and events. + .with_window_attributes_hook(window_attrs) + .build()?; slint::platform::set_platform(Box::new(backend))?; - // Create window + // Build UI and apply all properties let ui = OSDpopup::new()?; + set_ui_props(&ui, &cfg, args)?; - // Send the data to the UI as properties - let _ = set_ui_props(&ui, args); - - // Run the UI ui.run()?; - Ok(()) }