From 633467b6f256c8daa24d9a9af907082b43f9894c Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 17 May 2026 13:13:37 +0800 Subject: [PATCH] Initial commit --- assets/Battery.svg | 1 + assets/CPU.svg | 1 + assets/Power Draw.svg | 1 + assets/RAM Usage.svg | 1 + assets/WiFi Connecting.svg | 1 + assets/WiFi Medium.svg | 1 + assets/WiFi Strong.svg | 1 + assets/WiFi Weak.svg | 1 + src/bar/stats.rs | 95 ++++++++++++++++++++++++++++++-------- src/main.rs | 90 ++++++++++++++++++++++++++++-------- src/notifications/mod.rs | 2 +- src/notifications/popup.rs | 2 +- src/theme.rs | 2 +- 13 files changed, 158 insertions(+), 41 deletions(-) create mode 100644 assets/Battery.svg create mode 100644 assets/CPU.svg create mode 100644 assets/Power Draw.svg create mode 100644 assets/RAM Usage.svg create mode 100644 assets/WiFi Connecting.svg create mode 100644 assets/WiFi Medium.svg create mode 100644 assets/WiFi Strong.svg create mode 100644 assets/WiFi Weak.svg diff --git a/assets/Battery.svg b/assets/Battery.svg new file mode 100644 index 0000000..11b82f5 --- /dev/null +++ b/assets/Battery.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/CPU.svg b/assets/CPU.svg new file mode 100644 index 0000000..52c7437 --- /dev/null +++ b/assets/CPU.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/Power Draw.svg b/assets/Power Draw.svg new file mode 100644 index 0000000..dd5ea1f --- /dev/null +++ b/assets/Power Draw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/RAM Usage.svg b/assets/RAM Usage.svg new file mode 100644 index 0000000..434bdfe --- /dev/null +++ b/assets/RAM Usage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/WiFi Connecting.svg b/assets/WiFi Connecting.svg new file mode 100644 index 0000000..0cb213d --- /dev/null +++ b/assets/WiFi Connecting.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/WiFi Medium.svg b/assets/WiFi Medium.svg new file mode 100644 index 0000000..2b08f7f --- /dev/null +++ b/assets/WiFi Medium.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/WiFi Strong.svg b/assets/WiFi Strong.svg new file mode 100644 index 0000000..912158c --- /dev/null +++ b/assets/WiFi Strong.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/WiFi Weak.svg b/assets/WiFi Weak.svg new file mode 100644 index 0000000..59c7f31 --- /dev/null +++ b/assets/WiFi Weak.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/bar/stats.rs b/src/bar/stats.rs index bad7c9f..18f12b8 100644 --- a/src/bar/stats.rs +++ b/src/bar/stats.rs @@ -2,6 +2,21 @@ use crate::{App, AppInput}; use relm4::ComponentSender; use std::{fs, path::PathBuf, sync::Mutex}; +const WIFI_STRONG: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Strong.svg"); +const WIFI_MEDIUM: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Medium.svg"); +const WIFI_WEAK: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Weak.svg"); +const WIFI_OFF: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Connecting.svg"); + +#[derive(Debug)] +pub struct Stats { + pub cpu: String, + pub mem: String, + pub power: String, + pub bat: String, + pub wifi_ssid: String, + pub wifi_icon: &'static str, +} + struct CpuSnapshot { total: u64, idle: u64, @@ -35,7 +50,7 @@ fn read_cpu() -> f32 { (dtotal - didle) as f32 / dtotal as f32 * 100.0 } -fn read_ram() -> f32 { +fn read_ram() -> u64 { let text = fs::read_to_string("/proc/meminfo").unwrap_or_default(); let mut total = 0u64; let mut avail = 0u64; @@ -47,10 +62,7 @@ fn read_ram() -> f32 { _ => {} } } - if total == 0 { - return 0.0; - } - (total - avail) as f32 / total as f32 * 100.0 + total.saturating_sub(avail) } fn bat_path() -> Option { @@ -92,29 +104,76 @@ fn read_battery() -> Option { .ok() } -async fn read_wifi() -> String { - let out = tokio::process::Command::new("iw") +async fn read_wifi() -> (String, &'static str) { + let dev_out = tokio::process::Command::new("iw") .arg("dev") .output() .await .ok(); - let stdout = match out { + let dev_stdout = match dev_out { Some(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).into_owned(), - _ => return "—".into(), + _ => return ("—".into(), WIFI_OFF), }; - stdout + + let iface = dev_stdout .lines() - .find_map(|l| l.trim().strip_prefix("ssid ").map(str::to_string)) - .unwrap_or_else(|| "—".into()) + .find_map(|l| l.trim().strip_prefix("Interface ").map(str::to_string)); + let Some(iface) = iface else { + return ("—".into(), WIFI_OFF); + }; + + let link_out = tokio::process::Command::new("iw") + .args(["dev", &iface, "link"]) + .output() + .await + .ok(); + let link_stdout = match link_out { + Some(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).into_owned(), + _ => return ("—".into(), WIFI_OFF), + }; + + let mut ssid = None; + let mut rssi: Option = None; + for line in link_stdout.lines() { + let t = line.trim(); + if let Some(s) = t.strip_prefix("SSID: ") { + ssid = Some(s.to_string()); + } else if let Some(r) = t.strip_prefix("signal: ") { + rssi = r.split_whitespace().next().and_then(|s| s.parse().ok()); + } + } + + let Some(ssid) = ssid else { + return ("—".into(), WIFI_OFF); + }; + + let icon = match rssi { + Some(r) if r >= -55 => WIFI_STRONG, + Some(r) if r >= -70 => WIFI_MEDIUM, + _ => WIFI_WEAK, + }; + + (ssid, icon) } -pub async fn poll() -> String { +pub async fn poll() -> Stats { let cpu = read_cpu(); - let ram = read_ram(); - let power = read_power().map_or_else(|| "—W".into(), |w| format!("{w:.1}W")); - let bat = read_battery().map_or_else(|| "—".into(), |p| format!("{p}%")); - let wifi = read_wifi().await; - format!("CPU {cpu:.0}% MEM {ram:.0}% {power} BAT {bat} {wifi}") + let mem = read_ram(); + let power = read_power().map_or_else(|| " —W".into(), |w| format!("{w:4.1}W")); + let bat = read_battery().map_or_else(|| " —".into(), |p| format!("{p:3}%")); + let (wifi_ssid, wifi_icon) = read_wifi().await; + Stats { + cpu: format!("{cpu:3.0}%"), + mem: if mem >= 1024 * 1024 { + format!("{:.1}G", mem as f32 / (1024.0 * 1024.0)) + } else { + format!("{}M", mem / 1024) + }, + power, + bat, + wifi_ssid, + wifi_icon, + } } pub fn spawn_poller(sender: ComponentSender) { diff --git a/src/main.rs b/src/main.rs index f76d8d6..f4c8068 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,9 @@ +macro_rules! asset { + ($n:literal) => { + concat!(env!("CARGO_MANIFEST_DIR"), "/assets/", $n) + }; +} + mod bar; mod notifications; mod theme; @@ -12,9 +18,13 @@ pub struct App { workspaces: Vec, active_ws: WorkspaceId, time_str: String, - stats_str: String, - // GObject handle — manipulated directly in update() to avoid update_view conflicts. workspace_box: gtk4::Box, + cpu_lbl: gtk4::Label, + mem_lbl: gtk4::Label, + pwr_lbl: gtk4::Label, + bat_lbl: gtk4::Label, + wifi_lbl: gtk4::Label, + wifi_img: gtk4::Image, } #[derive(Debug)] @@ -22,7 +32,7 @@ pub enum AppInput { WorkspaceList(Vec), ActiveWorkspace(WorkspaceId), ClockTick, - StatsUpdate(String), + StatsUpdate(bar::stats::Stats), } #[relm4::component(pub)] @@ -37,6 +47,7 @@ impl SimpleComponent for App { set_title: Some("aster"), set_default_height: 32, + #[name = "center_box"] gtk::CenterBox { #[wrap(Some)] set_start_widget = >k::Box { @@ -56,18 +67,6 @@ impl SimpleComponent for App { #[watch] set_label: &model.time_str, }, - - #[wrap(Some)] - set_end_widget = >k::Box { - set_orientation: gtk::Orientation::Horizontal, - set_spacing: 8, - set_margin_end: 8, - - gtk::Label { - #[watch] - set_label: &model.stats_str, - } - }, } } } @@ -84,17 +83,42 @@ impl SimpleComponent for App { root.set_anchor(Edge::Right, true); root.set_exclusive_zone(32); + let cpu_lbl = stat_label(4); + let mem_lbl = stat_label(4); + let pwr_lbl = stat_label(5); + let bat_lbl = stat_label(4); + let wifi_lbl = gtk4::Label::new(None); + wifi_lbl.set_ellipsize(gtk4::pango::EllipsizeMode::End); + wifi_lbl.set_max_width_chars(12); + let wifi_img = gtk4::Image::from_paintable(Some(&svg_texture(asset!("WiFi Connecting.svg")))); + let mut model = App { workspaces: vec![], active_ws: 1, time_str: bar::clock::current(), - stats_str: String::new(), workspace_box: gtk4::Box::new(gtk4::Orientation::Horizontal, 4), + cpu_lbl: cpu_lbl.clone(), + mem_lbl: mem_lbl.clone(), + pwr_lbl: pwr_lbl.clone(), + bat_lbl: bat_lbl.clone(), + wifi_lbl: wifi_lbl.clone(), + wifi_img: wifi_img.clone(), }; let widgets = view_output!(); - model.workspace_box = widgets.workspace_box.clone(); + let stats_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 8); + stats_box.set_margin_end(8); + stats_box.append(&stat_pair(asset!("CPU.svg"), &cpu_lbl)); + stats_box.append(&stat_pair(asset!("RAM Usage.svg"), &mem_lbl)); + stats_box.append(&stat_pair(asset!("Power Draw.svg"), &pwr_lbl)); + stats_box.append(&stat_pair(asset!("Battery.svg"), &bat_lbl)); + let wifi_pair = gtk4::Box::new(gtk4::Orientation::Horizontal, 4); + wifi_pair.append(&wifi_img); + wifi_pair.append(&wifi_lbl); + stats_box.append(&wifi_pair); + widgets.center_box.set_end_widget(Some(&stats_box)); + theme::apply(); bar::workspaces::spawn_watcher(sender.clone()); bar::clock::spawn_ticker(sender.clone()); @@ -119,8 +143,13 @@ impl SimpleComponent for App { AppInput::ClockTick => { self.time_str = bar::clock::current(); } - AppInput::StatsUpdate(s) => { - self.stats_str = s; + AppInput::StatsUpdate(stats) => { + self.cpu_lbl.set_label(&stats.cpu); + self.mem_lbl.set_label(&stats.mem); + self.pwr_lbl.set_label(&stats.power); + self.bat_lbl.set_label(&stats.bat); + self.wifi_lbl.set_label(&stats.wifi_ssid); + self.wifi_img.set_paintable(Some(&svg_texture(stats.wifi_icon))); } } } @@ -138,8 +167,29 @@ impl App { } } +fn stat_pair(icon_path: &str, label: >k4::Label) -> gtk4::Box { + let pair = gtk4::Box::new(gtk4::Orientation::Horizontal, 4); + pair.append(>k4::Image::from_paintable(Some(&svg_texture(icon_path)))); + pair.append(label); + pair +} + +fn svg_texture(path: &str) -> gtk4::gdk::Texture { + let svg = std::fs::read_to_string(path) + .unwrap_or_default() + .replace("currentColor", "white"); + let bytes = gtk4::glib::Bytes::from_owned(svg.into_bytes()); + gtk4::gdk::Texture::from_bytes(&bytes).expect("svg load") +} + +fn stat_label(width_chars: i32) -> gtk4::Label { + let lbl = gtk4::Label::new(None); + lbl.set_width_chars(width_chars); + lbl.set_xalign(1.0); + lbl +} + fn main() { - // Reload theme CSS on SIGHUP (e.g. after pywal runs). relm4::spawn(async { use tokio::signal::unix::{signal, SignalKind}; let mut stream = signal(SignalKind::hangup()).expect("SIGHUP handler"); diff --git a/src/notifications/mod.rs b/src/notifications/mod.rs index c6ccafd..5ae7817 100644 --- a/src/notifications/mod.rs +++ b/src/notifications/mod.rs @@ -55,7 +55,7 @@ impl NotifServer { } fn get_server_information(&self) -> (String, String, String, String) { - ("aster".into(), "breadway".into(), "0.1.0".into(), "1.2".into()) + ("breadbar".into(), "breadway".into(), "0.1.0".into(), "1.2".into()) } } diff --git a/src/notifications/popup.rs b/src/notifications/popup.rs index 7340675..0cfa054 100644 --- a/src/notifications/popup.rs +++ b/src/notifications/popup.rs @@ -58,7 +58,7 @@ fn dismiss(cards_box: >k4::Box, window: >k4::Window, cards: &Cards, id: u32) fn create_window() -> gtk4::Window { let window = gtk4::Window::new(); - window.add_css_class("aster-notification"); + window.add_css_class("breadbar-notification"); window.init_layer_shell(); window.set_layer(Layer::Overlay); window.set_anchor(Edge::Top, true); diff --git a/src/theme.rs b/src/theme.rs index c11abed..55b95cb 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -44,7 +44,7 @@ fn load_css() -> String { }; format!( - "window.aster-bar {{ background-color: {bg_rgba}; }}\ + "window.breadbar {{ background-color: {bg_rgba}; }}\ .workspace-btn {{ background: {surface}; color: {fg}; border-radius: 4px;\ border: none; min-width: 24px; padding: 0 8px; }}\ .workspace-btn:hover, .workspace-btn.active {{ background: {accent}; }}\