Configuration crate

- Write config library crate
- Add comments and default values
- Update UI binary to add config loading
This commit is contained in:
Candifloss 2025-12-11 13:35:29 +05:30
parent 8343dc18f2
commit 77627207d8
6 changed files with 307 additions and 88 deletions

View File

@ -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(),
}
}
}

View File

@ -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<Self, ConfigError> {
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()
}
}

View File

@ -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(),
}
}
}

View File

@ -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(),
}
}
}

View File

@ -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(),
}
}
}

View File

@ -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<Color, String> {
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<Color, String> {
}
}
/// Set Slint properties from config data
fn set_ui_props(ui: &OSDpopup, args: &OsdArgs) -> Result<(), Box<dyn std::error::Error>> {
// 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<dyn std::error::Error>> {
// 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<dyn std::error::Error>> {
// 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(())
}