step 6+7: live clock, CPU/MEM/power/battery/WiFi stats poller
This commit is contained in:
parent
241e3bc2cb
commit
f1b471652b
3 changed files with 163 additions and 7 deletions
|
|
@ -1 +1,16 @@
|
||||||
|
use crate::{App, AppInput};
|
||||||
|
use relm4::ComponentSender;
|
||||||
|
|
||||||
|
pub fn current() -> String {
|
||||||
|
let dt = gtk4::glib::DateTime::now_local().expect("local time");
|
||||||
|
format!("{:02}:{:02}", dt.hour(), dt.minute())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_ticker(sender: ComponentSender<App>) {
|
||||||
|
relm4::spawn(async move {
|
||||||
|
loop {
|
||||||
|
sender.input(AppInput::ClockTick);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
126
src/bar/stats.rs
126
src/bar/stats.rs
|
|
@ -1 +1,127 @@
|
||||||
|
use crate::{App, AppInput};
|
||||||
|
use relm4::ComponentSender;
|
||||||
|
use std::{fs, path::PathBuf, sync::Mutex};
|
||||||
|
|
||||||
|
struct CpuSnapshot {
|
||||||
|
total: u64,
|
||||||
|
idle: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
static PREV_CPU: std::sync::OnceLock<Mutex<CpuSnapshot>> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
fn read_cpu() -> f32 {
|
||||||
|
let text = fs::read_to_string("/proc/stat").unwrap_or_default();
|
||||||
|
let line = text.lines().next().unwrap_or_default();
|
||||||
|
let vals: Vec<u64> = line
|
||||||
|
.split_whitespace()
|
||||||
|
.skip(1)
|
||||||
|
.filter_map(|s| s.parse().ok())
|
||||||
|
.collect();
|
||||||
|
if vals.len() < 5 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let idle = vals[3] + vals.get(4).copied().unwrap_or(0);
|
||||||
|
let total: u64 = vals.iter().sum();
|
||||||
|
|
||||||
|
let state = PREV_CPU.get_or_init(|| Mutex::new(CpuSnapshot { total, idle }));
|
||||||
|
let mut prev = state.lock().unwrap();
|
||||||
|
let dtotal = total.saturating_sub(prev.total);
|
||||||
|
let didle = idle.saturating_sub(prev.idle);
|
||||||
|
*prev = CpuSnapshot { total, idle };
|
||||||
|
|
||||||
|
if dtotal == 0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
(dtotal - didle) as f32 / dtotal as f32 * 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ram() -> f32 {
|
||||||
|
let text = fs::read_to_string("/proc/meminfo").unwrap_or_default();
|
||||||
|
let mut total = 0u64;
|
||||||
|
let mut avail = 0u64;
|
||||||
|
for line in text.lines() {
|
||||||
|
let mut parts = line.split_whitespace();
|
||||||
|
match parts.next() {
|
||||||
|
Some("MemTotal:") => total = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
Some("MemAvailable:") => avail = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total == 0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
(total - avail) as f32 / total as f32 * 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bat_path() -> Option<PathBuf> {
|
||||||
|
fs::read_dir("/sys/class/power_supply")
|
||||||
|
.ok()?
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.map(|e| e.path())
|
||||||
|
.find(|p| {
|
||||||
|
p.file_name()
|
||||||
|
.map_or(false, |n| n.to_string_lossy().starts_with("BAT"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_power() -> Option<f32> {
|
||||||
|
let path = bat_path()?;
|
||||||
|
if let Ok(v) = fs::read_to_string(path.join("power_now")) {
|
||||||
|
if let Ok(uw) = v.trim().parse::<u64>() {
|
||||||
|
return Some(uw as f32 / 1_000_000.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ua: u64 = fs::read_to_string(path.join("current_now"))
|
||||||
|
.ok()?
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.ok()?;
|
||||||
|
let uv: u64 = fs::read_to_string(path.join("voltage_now"))
|
||||||
|
.ok()?
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.ok()?;
|
||||||
|
Some((ua as f64 * uv as f64 / 1e12) as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_battery() -> Option<u8> {
|
||||||
|
fs::read_to_string(bat_path()?.join("capacity"))
|
||||||
|
.ok()?
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_wifi() -> String {
|
||||||
|
let out = tokio::process::Command::new("iw")
|
||||||
|
.arg("dev")
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
let stdout = match out {
|
||||||
|
Some(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).into_owned(),
|
||||||
|
_ => return "—".into(),
|
||||||
|
};
|
||||||
|
stdout
|
||||||
|
.lines()
|
||||||
|
.find_map(|l| l.trim().strip_prefix("ssid ").map(str::to_string))
|
||||||
|
.unwrap_or_else(|| "—".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn poll() -> String {
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_poller(sender: ComponentSender<App>) {
|
||||||
|
relm4::spawn(async move {
|
||||||
|
loop {
|
||||||
|
sender.input(AppInput::StatsUpdate(poll().await));
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
31
src/main.rs
31
src/main.rs
|
|
@ -11,7 +11,9 @@ use relm4::prelude::*;
|
||||||
pub struct App {
|
pub struct App {
|
||||||
workspaces: Vec<Workspace>,
|
workspaces: Vec<Workspace>,
|
||||||
active_ws: WorkspaceId,
|
active_ws: WorkspaceId,
|
||||||
// Stored handle so update() can manipulate the live widget directly.
|
time_str: String,
|
||||||
|
stats_str: String,
|
||||||
|
// GObject handle — manipulated directly in update() to avoid update_view conflicts.
|
||||||
workspace_box: gtk4::Box,
|
workspace_box: gtk4::Box,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,6 +21,8 @@ pub struct App {
|
||||||
pub enum AppInput {
|
pub enum AppInput {
|
||||||
WorkspaceList(Vec<Workspace>),
|
WorkspaceList(Vec<Workspace>),
|
||||||
ActiveWorkspace(WorkspaceId),
|
ActiveWorkspace(WorkspaceId),
|
||||||
|
ClockTick,
|
||||||
|
StatsUpdate(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[relm4::component(pub)]
|
#[relm4::component(pub)]
|
||||||
|
|
@ -49,7 +53,8 @@ impl SimpleComponent for App {
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
set_center_widget = >k::Label {
|
set_center_widget = >k::Label {
|
||||||
set_label: "00:00",
|
#[watch]
|
||||||
|
set_label: &model.time_str,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[wrap(Some)]
|
#[wrap(Some)]
|
||||||
|
|
@ -59,7 +64,8 @@ impl SimpleComponent for App {
|
||||||
set_margin_end: 8,
|
set_margin_end: 8,
|
||||||
|
|
||||||
gtk::Label {
|
gtk::Label {
|
||||||
set_label: "— — — —",
|
#[watch]
|
||||||
|
set_label: &model.stats_str,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -78,19 +84,21 @@ impl SimpleComponent for App {
|
||||||
root.set_anchor(Edge::Right, true);
|
root.set_anchor(Edge::Right, true);
|
||||||
root.set_exclusive_zone(32);
|
root.set_exclusive_zone(32);
|
||||||
|
|
||||||
// Placeholder until view_output! gives us the real handle.
|
|
||||||
let mut model = App {
|
let mut model = App {
|
||||||
workspaces: vec![],
|
workspaces: vec![],
|
||||||
active_ws: 1,
|
active_ws: 1,
|
||||||
|
time_str: bar::clock::current(),
|
||||||
|
stats_str: String::new(),
|
||||||
workspace_box: gtk4::Box::new(gtk4::Orientation::Horizontal, 4),
|
workspace_box: gtk4::Box::new(gtk4::Orientation::Horizontal, 4),
|
||||||
};
|
};
|
||||||
let widgets = view_output!();
|
let widgets = view_output!();
|
||||||
|
|
||||||
// Swap in the actual widget so update() can reach it.
|
|
||||||
model.workspace_box = widgets.workspace_box.clone();
|
model.workspace_box = widgets.workspace_box.clone();
|
||||||
|
|
||||||
theme::apply();
|
theme::apply();
|
||||||
bar::workspaces::spawn_watcher(sender);
|
bar::workspaces::spawn_watcher(sender.clone());
|
||||||
|
bar::clock::spawn_ticker(sender.clone());
|
||||||
|
bar::stats::spawn_poller(sender);
|
||||||
|
|
||||||
ComponentParts { model, widgets }
|
ComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
|
|
@ -101,13 +109,20 @@ impl SimpleComponent for App {
|
||||||
let mut sorted = list;
|
let mut sorted = list;
|
||||||
sorted.sort_by_key(|w| w.id);
|
sorted.sort_by_key(|w| w.id);
|
||||||
self.workspaces = sorted;
|
self.workspaces = sorted;
|
||||||
|
self.rebuild_buttons();
|
||||||
}
|
}
|
||||||
AppInput::ActiveWorkspace(id) => {
|
AppInput::ActiveWorkspace(id) => {
|
||||||
self.active_ws = id;
|
self.active_ws = id;
|
||||||
}
|
|
||||||
}
|
|
||||||
self.rebuild_buttons();
|
self.rebuild_buttons();
|
||||||
}
|
}
|
||||||
|
AppInput::ClockTick => {
|
||||||
|
self.time_str = bar::clock::current();
|
||||||
|
}
|
||||||
|
AppInput::StatsUpdate(s) => {
|
||||||
|
self.stats_str = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue