git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commit82e0640ee839c3f54741b7ce33fce4e90b5c6110
parentcbb1bc8216
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 21:58
feat(system): Builds section shows build durations, not ages

Each installed cce binary now lists how long its last build took — its
own bin unit plus its crate's lib and build script — read from the
record ccebuild keeps at ~/.local/state/cce/build-times (filled from
cargo's --timings reports). Slowest first; a binary no ccebuild build
has timed shows "—".

The age logic it replaces (build_time, the target/release comparison)
goes with it, and timers' humanize is private again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/pages/system_info.rs | 152 ++++++++++++++++++++---------------------------
 src/pages/timers.rs      |   2 +-
 2 files changed, 67 insertions(+), 87 deletions(-)

diff --git a/src/pages/system_info.rs b/src/pages/system_info.rs
index 4a1f40c..a0e798f 100644
--- a/src/pages/system_info.rs
+++ b/src/pages/system_info.rs
@@ -285,68 +285,61 @@ fn spawn_systemctl_force(action: &str) {
     let _ = crate::spawn_detached(cmd);
 }
 
-/// One compiled cce binary in `~/.local/bin` and when it was built.
+/// One compiled cce binary in `~/.local/bin` and how long it took to build.
 #[derive(Debug, Clone, PartialEq)]
 pub struct InstalledBuild {
     pub name: String,
-    pub built: std::time::SystemTime,
-    /// `target/release` holds a newer build than the one installed — built
-    /// but never deployed, which is half of what `ccebuild status` reports.
-    pub newer_uninstalled: bool,
+    /// Seconds the last recorded build of this binary took: its own bin unit
+    /// plus its crate's lib and build script, not its dependencies. `None`
+    /// when no build of it went through `ccebuild`, the only thing that
+    /// records them.
+    pub seconds: Option<f32>,
 }
 
-/// When the installed copy of a binary was built, and whether a newer build
-/// is waiting. Takes `(mtime, size)` of the installed file and, if present, of
-/// the one in `target/release`.
-///
-/// `ccebuild install` runs plain `install`, which does not preserve mtimes, so
-/// the installed file's own mtime is when it was INSTALLED. A whole-DE sweep
-/// installs long after the first crates finished building, so that is not the
-/// build time. When the build in `target/release` is no newer than the
-/// installed copy and the same size, it is the build that was installed, and
-/// its mtime is the real answer. Otherwise the build that was installed has
-/// since been overwritten (or never came from this tree), and the install
-/// time is the best bound left — the build can be no later than that.
-fn build_time(
-    installed: (std::time::SystemTime, u64),
-    built: Option<(std::time::SystemTime, u64)>,
-) -> (std::time::SystemTime, bool) {
-    match built {
-        Some((b_mtime, _)) if b_mtime > installed.0 => (installed.0, true),
-        Some((b_mtime, b_size)) if b_size == installed.1 => (b_mtime, false),
-        _ => (installed.0, false),
-    }
+/// `ccebuild`'s build-time record, `bin<TAB>seconds<TAB>epoch` per line
+/// (`BUILD_TIMES` in the script, filled from cargo's `--timings` reports).
+/// Cargo keeps no build durations of its own, so a binary built by a bare
+/// `cargo build` has no line here.
+fn parse_build_times(text: &str) -> std::collections::HashMap<String, f32> {
+    text.lines()
+        .filter_map(|l| {
+            let mut f = l.split('\t');
+            let bin = f.next()?;
+            let secs = f.next()?.parse::<f32>().ok()?;
+            Some((bin.to_string(), secs))
+        })
+        .collect()
 }
 
-/// The workspace `ccebuild` last installed from, which it records for itself
-/// (`remember_workspace`). An app launched from the menu has no other way to
-/// find the tree.
-fn ccebuild_workspace() -> Option<std::path::PathBuf> {
-    let state = std::env::var_os("XDG_STATE_HOME")
-        .map(std::path::PathBuf::from)
-        .or_else(|| std::env::var_os("HOME").map(|h| std::path::Path::new(&h).join(".local/state")))?;
-    let ws = std::fs::read_to_string(state.join("cce/workspace")).ok()?;
-    let ws = std::path::PathBuf::from(ws.trim());
-    ws.is_dir().then_some(ws)
+/// "8.4s" / "1min 32s" — a build time, which unlike an age wants its tenths
+/// under a minute.
+fn format_build_duration(secs: f32) -> String {
+    // Rounded first, so 59.96 reads "1min 00s" rather than "60.0s".
+    if (secs * 10.0).round() < 600.0 {
+        format!("{:.1}s", secs)
+    } else {
+        let s = secs.round() as u64;
+        format!("{}min {:02}s", s / 60, s % 60)
+    }
 }
 
-/// Every compiled `cce*` binary in `~/.local/bin`, oldest build first — the
-/// order that matters, since an app left behind by a toolkit sweep is stale
-/// against `cce-ui` in a way no built-vs-installed check can see, and its age
-/// is the only tell. Symlinks (`cce`, `cce-settings`) are aliases, and shell
-/// scripts (`ccebuild`, `cce-shadow`) have no build, so only ELF files count.
+/// Every compiled `cce*` binary in `~/.local/bin` with its recorded build
+/// time, slowest first and unrecorded ones last. Symlinks (`cce`,
+/// `cce-settings`) are aliases, and shell scripts (`ccebuild`, `cce-shadow`)
+/// have no build, so only ELF files count.
 fn scan_installed_builds() -> Vec<InstalledBuild> {
     use std::io::Read;
     let Some(home) = std::env::var_os("HOME") else { return Vec::new() };
-    let bindir = std::path::Path::new(&home).join(".local/bin");
-    let release = ccebuild_workspace().map(|ws| ws.join("target/release"));
+    let home = std::path::Path::new(&home);
+    let bindir = home.join(".local/bin");
+    let state = std::env::var_os("XDG_STATE_HOME")
+        .map(std::path::PathBuf::from)
+        .unwrap_or_else(|| home.join(".local/state"));
+    let times = std::fs::read_to_string(state.join("cce/build-times"))
+        .map(|t| parse_build_times(&t))
+        .unwrap_or_default();
     let Ok(entries) = std::fs::read_dir(&bindir) else { return Vec::new() };
 
-    let stat = |p: &std::path::Path| -> Option<(std::time::SystemTime, u64)> {
-        let m = std::fs::metadata(p).ok()?;
-        Some((m.modified().ok()?, m.len()))
-    };
-
     let mut builds: Vec<InstalledBuild> = entries
         .filter_map(|e| e.ok())
         .filter_map(|e| {
@@ -354,19 +347,21 @@ fn scan_installed_builds() -> Vec<InstalledBuild> {
             if !name.starts_with("cce") || !e.file_type().ok()?.is_file() {
                 return None;
             }
-            let path = e.path();
             let mut magic = [0u8; 4];
-            std::fs::File::open(&path).ok()?.read_exact(&mut magic).ok()?;
+            std::fs::File::open(e.path()).ok()?.read_exact(&mut magic).ok()?;
             if &magic != b"\x7fELF" {
                 return None;
             }
-            let installed = stat(&path)?;
-            let built = release.as_ref().and_then(|r| stat(&r.join(&name)));
-            let (built, newer_uninstalled) = build_time(installed, built);
-            Some(InstalledBuild { name, built, newer_uninstalled })
+            let seconds = times.get(&name).copied();
+            Some(InstalledBuild { name, seconds })
         })
         .collect();
-    builds.sort_by(|a, b| a.built.cmp(&b.built).then_with(|| a.name.cmp(&b.name)));
+    builds.sort_by(|a, b| match (a.seconds, b.seconds) {
+        (Some(x), Some(y)) => y.total_cmp(&x).then_with(|| a.name.cmp(&b.name)),
+        (Some(_), None) => std::cmp::Ordering::Less,
+        (None, Some(_)) => std::cmp::Ordering::Greater,
+        (None, None) => a.name.cmp(&b.name),
+    });
     builds
 }
 
@@ -654,9 +649,7 @@ pub fn view(state: &mut SystemState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
     });
 
     // ── 6. Builds Section ──
-    // When each installed cce binary was built. Oldest first: a client nobody
-    // rebuilt after a toolkit change is stale in a way `ccebuild status`
-    // cannot see, and sinks to the top here by its age alone.
+    // How long each installed cce binary took to build, slowest first.
     builder.add_section(&mut final_pc, "Builds", sec_focused.get(5).copied().unwrap_or(false), |sec| {
         if !state.loaded {
             sec.text("Loading installed builds...", 12.0, 0.0, 12.0, TEXT_FG);
@@ -666,21 +659,16 @@ pub fn view(state: &mut SystemState, cx: f32, cy: f32, cw: f32, ch: f32, _root_f
             sec.text("No cce binaries in ~/.local/bin", 12.0, 0.0, 12.0, TEXT_DIM);
             return;
         }
-        let now = std::time::SystemTime::now();
         let mut stack = sec.vstack(0.0);
         for b in &state.builds {
-            let age = now.duration_since(b.built).map(|d| d.as_secs()).unwrap_or(0);
-            let (when, col) = if b.newer_uninstalled {
-                // Fits the column; the full story is in `build_time`.
-                (format!("{} ago · newer build", crate::pages::timers::humanize(age)), DANGER_BG)
-            } else {
-                (format!("{} ago", crate::pages::timers::humanize(age)), TEXT_DIM)
-            };
+            // "—" is a binary no ccebuild build has timed (a bare `cargo
+            // build`, or one built before the timing was recorded).
+            let took = b.seconds.map(format_build_duration).unwrap_or_else(|| "—".to_string());
             let name = b.name.clone();
             stack.add_row(1, 0.0, 16.0, move |c, _, x, _| {
                 let y = c.ay();
                 c.pc.text(&name, x, y + 3.0, 11.0, TEXT_FG);
-                c.pc.text(&when, x + 140.0, y + 3.0, 11.0, col);
+                c.pc.text(&took, x + 140.0, y + 3.0, 11.0, TEXT_DIM);
             });
         }
     });
@@ -859,29 +847,21 @@ mod tests {
         assert!(parse_pending("==> system artifacts already up to date\n").is_empty());
     }
 
-    fn at(secs: u64) -> std::time::SystemTime {
-        std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs)
-    }
-
-    /// The installed build is the one in target/release: report when it was
-    /// built, not when the sweep got around to installing it.
-    #[test]
-    fn build_time_prefers_the_matching_build() {
-        assert_eq!(build_time((at(200), 10), Some((at(100), 10))), (at(100), false));
-    }
-
-    /// A newer build that was never installed: the installed binary came from
-    /// an overwritten build, so the install time is the only bound left.
+    /// The record is ccebuild's TSV; a malformed line (a hand edit, a
+    /// half-written file) drops out rather than failing the whole section.
     #[test]
-    fn build_time_flags_a_newer_uninstalled_build() {
-        assert_eq!(build_time((at(200), 10), Some((at(300), 12))), (at(200), true));
+    fn parse_build_times_reads_ccebuild_lines() {
+        let t = parse_build_times("cce-mail\t83.27\t1791251542\ncce-ui\t4.1\t1791251542\nbroken line\ncce-x\tnan?\t1\n");
+        assert_eq!(t.len(), 2);
+        assert_eq!(t.get("cce-mail"), Some(&83.27));
+        assert_eq!(t.get("cce-ui"), Some(&4.1));
     }
 
-    /// Older but a different size: not the build that was installed.
     #[test]
-    fn build_time_ignores_a_build_that_does_not_match() {
-        assert_eq!(build_time((at(200), 10), Some((at(100), 12))), (at(200), false));
-        assert_eq!(build_time((at(200), 10), None), (at(200), false));
+    fn format_build_duration_keeps_tenths_under_a_minute() {
+        assert_eq!(format_build_duration(8.44), "8.4s");
+        assert_eq!(format_build_duration(92.0), "1min 32s");
+        assert_eq!(format_build_duration(59.96), "1min 00s");
     }
 
     #[test]
diff --git a/src/pages/timers.rs b/src/pages/timers.rs
index dff28f2..5697a2e 100644
--- a/src/pages/timers.rs
+++ b/src/pages/timers.rs
@@ -83,7 +83,7 @@ pub enum TimersMessage {
 const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
 
 /// "46min" / "3h" / "5d 3h" — coarse two-unit humanization.
-pub(crate) fn humanize(secs: u64) -> String {
+fn humanize(secs: u64) -> String {
     let (d, h, m) = (secs / 86_400, (secs % 86_400) / 3600, (secs % 3600) / 60);
     if d > 0 {
         if h > 0 { format!("{}d {}h", d, h) } else { format!("{}d", d) }