diff --git a/Cargo.toml b/Cargo.toml index ce78686..4ca3b0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ dirs = "6.0.0" i-slint-backend-winit = { version = "1.14.1", features = ["x11"] } serde = { version = "1.0.228", features = ["derive"] } slint = { version = "1.14.1", features = ["backend-winit"] } -tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "time"] } toml = "0.9.8" [build-dependencies] diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index c8b2d4a..dd6e650 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -2,6 +2,7 @@ mod batterywidget; mod common; mod datewidget; mod timewidget; +mod volumewidget; use crate::TopBar; @@ -9,6 +10,7 @@ pub fn install_callbacks(ui: &TopBar) { timewidget::install(ui); datewidget::install(ui); batterywidget::install(ui); + volumewidget::install(ui); // In the future: // networkwidget::install(ui); diff --git a/src/widgets/volumewidget.rs b/src/widgets/volumewidget.rs new file mode 100644 index 0000000..1367bd3 --- /dev/null +++ b/src/widgets/volumewidget.rs @@ -0,0 +1,97 @@ +use super::common::run_cmd; +use crate::TopBar; +use slint::{ComponentHandle, SharedString, Timer, TimerMode}; +use std::process::Command; +use std::time::Duration; + +// Run `amixer get Master` and return stdout as String +fn read_amixer() -> String { + String::from_utf8( + Command::new("amixer") + .args(["get", "Master"]) + .output() + .unwrap() + .stdout, + ) + .unwrap_or_default() +} + +// Parse volume percentage (0–100) +fn parse_volume(amixer_out: &str) -> u8 { + // Looks for "[42%]" pattern + for token in amixer_out.split_whitespace() { + if let Some(stripped) = token.strip_prefix('[').and_then(|t| t.strip_suffix("%]")) + && let Ok(val) = stripped.parse::() + { + return val; + } + } + 0 +} + +// Detect mute: `[off]` means muted +fn is_muted(amixer_out: &str) -> bool { + amixer_out.contains("[off]") +} + +// Select simple volume icon from level +fn select_icon(vol: u8, muted: bool) -> SharedString { + let icon = if muted { + "" // mute + } else if vol == 0 { + "" // empty + } else if vol <= 30 { + "" // low + } else if vol <= 70 { + "" // medium + } else { + "" // high + }; + + SharedString::from(icon) +} + +// Build tooltip text +fn make_tooltip(vol: u8, muted: bool) -> SharedString { + if muted { + SharedString::from(format!("Volume: {vol}%")) + } else { + SharedString::from("Volume: {vol}%") + } +} + +// Timer loop: update volume every second +fn start_volume_updater(ui: &TopBar) { + let weak = ui.as_weak(); + + let timer = Box::leak(Box::new(Timer::default())); + timer.start(TimerMode::Repeated, Duration::from_secs(1), move || { + if let Some(ui) = weak.upgrade() { + let out = read_amixer(); + //println!("{out}"); + let vol = parse_volume(&out); + let muted = is_muted(&out); + + // Compute values + let icon = select_icon(vol, muted); + let tooltip = make_tooltip(vol, muted); + + // Push to UI + ui.set_volume_icon(icon); + ui.set_volume_tooltip(tooltip); + } + }); +} + +// Public entry: connect callback + start updater +pub fn install(ui: &TopBar) { + let weak = ui.as_weak(); + + ui.on_show_volume(move || { + if weak.upgrade().is_some() { + run_cmd("xclock"); // placeholder + } + }); + + start_volume_updater(ui); +} diff --git a/ui/topbar.slint b/ui/topbar.slint index 91b6459..b90a04c 100644 --- a/ui/topbar.slint +++ b/ui/topbar.slint @@ -21,13 +21,22 @@ export component TopBar inherits Window { // Battery widget in property battery_tooltip; in property battery_icon; - in property battery_icon_color: #ffffff; - in property battery_bg_normal: #8e9162; - in property battery_bg_hover: #6d8a4d; - in property battery_bg_clicked: #4a5f70; + in property battery_icon_color; + /*in property battery_bg_normal; // Enable later. Use defaults now. + in property battery_bg_hover; + in property battery_bg_clicked;*/ callback show_battery(); + // Volume widget + in property volume_tooltip; + in property volume_icon; + /*in property volume_icon_color; + in property volume_bg_normal; + in property volume_bg_hover; + in property volume_bg_clicked;*/ + callback show_volume(); + title: "chocobar"; width: bar_width *1px; height: bar_height *1px; @@ -71,16 +80,28 @@ export component TopBar inherits Window { HorizontalLayout { alignment: end; // Right-align + + // SquareIconWidget - Volume + SquareIconWidget { + bar_height: root.bar_height; + icon_text: root.volume_icon; + tooltip_text: root.volume_tooltip; + /*icon_color: root.volume_icon_color; + bg_normal: root.volume_bg_normal; + bg_hover: root.volume_bg_hover; + bg_clicked: root.volume_bg_clicked;*/ + square_btn_callback => root.show_volume(); + } - // SquareIconWidget - Battery + // SquareIconWidget - Battery SquareIconWidget { bar_height: root.bar_height; icon_text: root.battery_icon; tooltip_text: root.battery_tooltip; icon_color: root.battery_icon_color; - bg_normal: root.battery_bg_normal; + /*bg_normal: root.battery_bg_normal; bg_hover: root.battery_bg_hover; - bg_clicked: root.battery_bg_clicked; + bg_clicked: root.battery_bg_clicked;*/ square_btn_callback => root.show_battery(); }