status bar
git clone https://git.lucas.co/cce-status-interface.git
feat(memory): the memory module is an icon readout, in percent
The reader now returns memory in use as a whole percentage of the total
(used = total less free, buffers and page cache) instead of the
"Mem 10/62G" string, and the module joins the IconStat set on the new
cce-icons memory glyph, with "Mem 16%" as its text fallback.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 12 ++++----
src/main.rs | 8 ++++--
src/modules.rs | 87 ++++++++++------------------------------------------------
src/stats.rs | 21 ++++++++------
4 files changed, 39 insertions(+), 89 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index f8325a1..90c1ee2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -86,7 +86,7 @@ Orientation is dynamic: `is_vertical()` compares the surface size against the
configured bar thickness; every module renders along one axis using `bar_h`/`coord`
accordingly.
-**The percentage modules read out as a glyph, not a label.** `cpu`,
+**The stat modules read out as a glyph, not a label.** `cpu`, `memory`,
`brightness`, `volume` and `battery` are `IconStat` implementations (a
blanket `impl<T: IconStat> StatusModule for T` in `modules.rs` does the shared
layout): each names a cce-icons glyph, the bare number and a color, and
@@ -120,7 +120,9 @@ needs `CCE_ICONS_DIR` exported into the spawn**, its HOME being elsewhere,
exactly as it needs `CCE_FONTS_DIR`. Slot stability holds as before: the
stable width is the wider of the glyph and the "100" template, and the glyph
normally wins, so a value crossing a digit boundary never resizes the
-surface. `memory` is not a percentage ("Mem 10/62G") and stays a label.
+surface. `memory` reads as a percentage of the total in use (used = total
+less free, buffers and page cache) since 2026-09-16 — the "Mem 10/62G"
+gigabyte form went with the label.
## Events and IPC
@@ -141,9 +143,9 @@ listen to tray D-Bus, etc.:
first. (The old `viewport` topic is gone with the viewport-tag feature.)
- **System stats** (`spawn_system_stats`): `/proc/stat`, `/proc/meminfo`,
`/sys/class/power_supply/BAT*`, `/sys/class/backlight`, and `pactl` for volume/mute.
- `SystemStats` carries numbers (`cpu_pct`, `battery: (capacity, charging)`,
- `volume: (level, muted)`, `brightness`), each `Option` where the source can
- be absent; only clock and memory arrive pre-formatted.
+ `SystemStats` carries numbers (`cpu_pct`, `memory`, `battery: (capacity,
+ charging)`, `volume: (level, muted)`, `brightness`), each `Option` where
+ the source can be absent; only the clock arrives pre-formatted.
- **Tray** (`spawn_status_tray`): a full StatusNotifierItem/Watcher host over `zbus`,
including DBusMenu fetching. Icons arrive as pixmaps or theme names (rendered via
`resvg`/`png`).
diff --git a/src/main.rs b/src/main.rs
index cea0362..f7490a5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -58,7 +58,9 @@ pub struct TrayIconBounds {
#[derive(Debug, Clone)]
pub struct SystemStats {
pub clock: String,
- pub memory: String,
+ /// Memory in use as a whole percentage of the total; `None` when
+ /// /proc/meminfo is unreadable.
+ pub memory: Option<u8>,
/// CPU busy share as a whole percentage; `None` when /proc/stat is
/// unreadable.
pub cpu_pct: Option<u8>,
@@ -114,13 +116,13 @@ fn stats_signature(module: Option<&str>, s: &SystemStats) -> Option<String> {
match module {
Some("clock") => Some(s.clock.clone()),
Some("cpu") => Some(format!("{:?}", s.cpu_pct)),
- Some("memory") => Some(s.memory.clone()),
+ Some("memory") => Some(format!("{:?}", s.memory)),
Some("battery") => Some(format!("{:?}", s.battery)),
Some("volume") => Some(format!("{:?}", s.volume)),
Some("brightness") => Some(format!("{:?}", s.brightness)),
Some("window") | Some("tray") | Some("light_source") => None,
_ => Some(format!(
- "{}|{}|{:?}|{:?}|{:?}|{:?}",
+ "{}|{:?}|{:?}|{:?}|{:?}|{:?}",
s.clock, s.memory, s.cpu_pct, s.battery, s.volume, s.brightness
)),
}
diff --git a/src/modules.rs b/src/modules.rs
index e912504..a84b53a 100644
--- a/src/modules.rs
+++ b/src/modules.rs
@@ -587,80 +587,21 @@ impl IconStat for BrightnessModule {
pub struct MemoryModule;
-impl MemoryModule {
- fn live_text<'a>(stats: &'a Option<SystemStats>) -> &'a str {
- match stats {
- Some(s) if !s.memory.is_empty() => &s.memory,
- _ => "Mem 0/0G",
- }
- }
-}
-
-impl StatusModule for MemoryModule {
- fn name(&self) -> &'static str { "memory" }
-
- fn width(
- &self,
- stats: &Option<SystemStats>,
- _title: &str,
- font_system: &mut FontSystem,
- font_family: &str,
- font_size: f32,
- _tray_items: &HashMap<String, TrayItem>,
- padding: f32,
- ) -> f32 {
- let text = Self::live_text(stats);
- // "Mem 17/62G" → "Mem 62/62G": used pinned to the total, the
- // widest this machine's readout gets.
- let template = text
- .rsplit('/')
- .next()
- .and_then(|total| total.strip_suffix('G'))
- .map(|total| format!("Mem {total}/{total}G"))
- .unwrap_or_else(|| text.to_string());
- stable_text_width(font_system, text, &template, font_size, font_family, padding)
- }
+impl IconStat for MemoryModule {
+ const NAME: &'static str = "memory";
- fn content_width(
- &self,
- stats: &Option<SystemStats>,
- _title: &str,
- font_system: &mut FontSystem,
- font_family: &str,
- font_size: f32,
- _tray_items: &HashMap<String, TrayItem>,
- padding: f32,
- ) -> f32 {
- live_text_width(font_system, Self::live_text(stats), font_size, font_family, padding)
- }
-
- fn render(
- &self,
- x: f32,
- _w: f32,
- stats: &Option<SystemStats>,
- _title: &str,
- font_system: &mut FontSystem,
- font_family: &str,
- font_size: f32,
- normal_color: [f32; 4],
- bar_h: f32,
- _scale_factor: f64,
- text_prims: &mut Vec<crate::TextPrim>,
- _icon_prims: &mut Vec<crate::IconPrim>,
- _rects: &mut Vec<RectWidget>,
- _overlay_rects: &mut Vec<RectWidget>,
- _tray_items: &HashMap<String, TrayItem>,
- _tray_item_bounds: &mut Vec<TrayIconBounds>,
- _box_bg_color: Option<[f32; 4]>,
- _status_box_radius: f32,
- _rounded_boxes: &mut Vec<RoundedBox>,
- padding: f32,
- ) {
- if let Some(ref s) = stats {
- let label = Label::new_with_family(font_system, &s.memory, font_size, normal_color, font_family);
- crate::draw_label(text_prims, label, x + padding, centered_text_y(bar_h, font_size));
- }
+ fn readout(stats: &Option<SystemStats>, normal_color: [f32; 4]) -> Option<IconReadout> {
+ let pct = match stats {
+ Some(s) => s.memory,
+ None => Some(0),
+ };
+ Some(IconReadout {
+ icon: "memory",
+ number: pct.map(|p| p.to_string()),
+ color: normal_color,
+ fallback: pct.map_or("Mem N/A".to_string(), |p| format!("Mem {p}%")),
+ fallback_template: "Mem 100%",
+ })
}
}
diff --git a/src/stats.rs b/src/stats.rs
index dfc8f54..4675818 100644
--- a/src/stats.rs
+++ b/src/stats.rs
@@ -21,26 +21,31 @@ pub(crate) fn read_cpu_ticks() -> Option<(u64, u64)> {
None
}
-pub(crate) fn read_memory_usage() -> Option<String> {
+/// Memory in use as a whole percentage of the total — used being total less
+/// free, buffers and page cache, so what an application could not have
+/// without the kernel dropping cache first. `None` when /proc/meminfo is
+/// unreadable.
+pub(crate) fn read_memory_usage() -> Option<u8> {
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
let mut total = 0.0;
let mut free = 0.0;
let mut buffers = 0.0;
let mut cached = 0.0;
for line in meminfo.lines() {
+ let kib = |line: &str| line.split_whitespace().nth(1).and_then(|v| v.parse::<f32>().ok());
if line.starts_with("MemTotal:") {
- total = line.split_whitespace().nth(1)?.parse::<f32>().ok()? / 1024.0 / 1024.0;
+ total = kib(line)?;
} else if line.starts_with("MemFree:") {
- free = line.split_whitespace().nth(1)?.parse::<f32>().ok()? / 1024.0 / 1024.0;
+ free = kib(line)?;
} else if line.starts_with("Buffers:") {
- buffers = line.split_whitespace().nth(1)?.parse::<f32>().ok()? / 1024.0 / 1024.0;
+ buffers = kib(line)?;
} else if line.starts_with("Cached:") {
- cached = line.split_whitespace().nth(1)?.parse::<f32>().ok()? / 1024.0 / 1024.0;
+ cached = kib(line)?;
}
}
if total > 0.0 {
let used = total - free - buffers - cached;
- Some(format!("Mem {:.0}/{:.0}G", used, total))
+ Some((used / total * 100.0).round().clamp(0.0, 100.0) as u8)
} else {
None
}
@@ -133,7 +138,7 @@ pub(crate) async fn read_volume() -> Option<(Option<u32>, bool)> {
pub(crate) fn get_initial_stats() -> SystemStats {
let clock = chrono::Local::now().format("%A, %B %d, %Y %I:%M %p").to_string();
- let memory = read_memory_usage().unwrap_or_else(|| "Mem N/A".to_string());
+ let memory = read_memory_usage();
SystemStats {
clock,
@@ -151,7 +156,7 @@ pub(crate) async fn spawn_system_stats(sender: calloop::channel::Sender<CustomEv
loop {
log::debug!("[spawn_system_stats] loop iteration start");
let clock = chrono::Local::now().format("%A, %B %d, %Y %I:%M %p").to_string();
- let memory = read_memory_usage().unwrap_or_else(|| "Mem N/A".to_string());
+ let memory = read_memory_usage();
let cpu_pct = if let Some(current_cpu) = read_cpu_ticks() {
let total_diff = current_cpu.0 - last_cpu.0;