ffbe098ef8
TLC (Twilight Commander) was missing from both ISO configs. Added
tlc = {} to [packages] in redbear-mini.toml and redbear-full.toml.
Created missing symlink: recipes/tui/tlc -> ../../local/recipes/tui/tlc.
198 lines
6.1 KiB
Rust
198 lines
6.1 KiB
Rust
//! StatusReporter — one-line cook progress for the non-TUI path.
|
|
//!
|
|
//! When the cookbook runs without its ratatui TUI (e.g. `CI=1 repo cook`
|
|
//! in a background shell, or via SSH), the only progress output is the
|
|
//! per-recipe tail of the build script. There's no aggregate
|
|
//! "5/15 done" view, no per-phase signal (fetch vs build vs package),
|
|
//! and no elapsed time. StatusReporter fills that gap with one-line
|
|
//! status updates to stderr.
|
|
//!
|
|
//! Activated automatically by the cookbook CLI when the TUI is off
|
|
//! AND log capture is off (i.e., `CI=1` mode). When the TUI is on,
|
|
//! the user already sees aggregate progress via ratatui; when log
|
|
//! capture is on, the per-recipe log file in `build/logs/` provides
|
|
//! the per-recipe context. In both of those cases StatusReporter is
|
|
//! a no-op so it never duplicates the existing UX.
|
|
//!
|
|
//! Output format:
|
|
//! `[05/15] kf6-kio: starting`
|
|
//! `[05/15] kf6-kio: fetched (3.2s)`
|
|
//! `[05/15] kf6-kio: built (4m 18s)`
|
|
//! `[05/15] kf6-kio: done (total 4m 23s)`
|
|
//!
|
|
//! All output is `eprintln!` so it never gets mixed with the
|
|
//! captured log output of the build script.
|
|
//!
|
|
//! Per build-system improvement #4 in
|
|
//! `local/docs/BUILD-SYSTEM-IMPROVEMENTS.md`.
|
|
|
|
use std::io::IsTerminal;
|
|
use std::time::Instant;
|
|
|
|
/// Returns true if the cook status reporter should emit progress
|
|
/// lines. Auto-enables when stdin AND stderr are both TTYs AND
|
|
/// neither the TUI nor log capture is wanted. (The TUI is the
|
|
/// ratatui dashboard; log capture writes per-recipe build logs to
|
|
/// `build/logs/<recipe>.log` for postmortem review.)
|
|
pub fn status_reporter_enabled(tui: bool, logs: bool) -> bool {
|
|
!tui && !logs && std::io::stderr().is_terminal()
|
|
}
|
|
|
|
pub struct StatusReporter {
|
|
enabled: bool,
|
|
index: usize,
|
|
total: usize,
|
|
name: String,
|
|
start: Instant,
|
|
phase_start: Instant,
|
|
last_phase: String,
|
|
}
|
|
|
|
impl StatusReporter {
|
|
/// Create a per-recipe status reporter. `index` is 1-based.
|
|
/// If `enabled` is false, all methods are no-ops and the
|
|
/// reporter can be dropped without effect.
|
|
pub fn new(enabled: bool, index: usize, total: usize, name: &str) -> Self {
|
|
Self {
|
|
enabled,
|
|
index,
|
|
total,
|
|
name: name.to_string(),
|
|
start: Instant::now(),
|
|
phase_start: Instant::now(),
|
|
last_phase: String::new(),
|
|
}
|
|
}
|
|
|
|
/// Emit a "starting" line. Call once per recipe.
|
|
pub fn start(&mut self) {
|
|
if !self.enabled {
|
|
return;
|
|
}
|
|
self.phase_start = Instant::now();
|
|
eprintln!(
|
|
"[{:02}/{:02}] {}: starting",
|
|
self.index, self.total, self.name,
|
|
);
|
|
}
|
|
|
|
/// Emit a phase transition. The `phase` arg is a short label
|
|
/// like "fetched", "building", "built", "packaging", "done".
|
|
/// Elapsed time printed is from the previous phase boundary
|
|
/// (or the recipe start for the first phase).
|
|
pub fn phase(&mut self, phase: &str) {
|
|
if !self.enabled {
|
|
return;
|
|
}
|
|
let phase_elapsed = self.phase_start.elapsed();
|
|
eprintln!(
|
|
"[{:02}/{:02}] {}: {} ({})",
|
|
self.index,
|
|
self.total,
|
|
self.name,
|
|
phase,
|
|
format_elapsed(phase_elapsed),
|
|
);
|
|
self.last_phase = phase.to_string();
|
|
self.phase_start = Instant::now();
|
|
}
|
|
|
|
/// Emit the final "done" line with total elapsed time.
|
|
pub fn done(&mut self) {
|
|
if !self.enabled {
|
|
return;
|
|
}
|
|
eprintln!(
|
|
"[{:02}/{:02}] {}: done (total {})",
|
|
self.index,
|
|
self.total,
|
|
self.name,
|
|
format_elapsed(self.start.elapsed()),
|
|
);
|
|
self.last_phase = "done".to_string();
|
|
}
|
|
|
|
pub fn cached(&mut self) {
|
|
if !self.enabled {
|
|
return;
|
|
}
|
|
eprintln!(
|
|
"[{:02}/{:02}] {}: cached",
|
|
self.index, self.total, self.name,
|
|
);
|
|
}
|
|
|
|
pub fn last_phase(&self) -> &str {
|
|
&self.last_phase
|
|
}
|
|
}
|
|
|
|
fn format_elapsed(d: std::time::Duration) -> String {
|
|
let total = d.as_secs();
|
|
if total < 60 {
|
|
format!("{}s", total)
|
|
} else if total < 3600 {
|
|
format!("{}m {:02}s", total / 60, total % 60)
|
|
} else {
|
|
format!("{}h {:02}m {:02}s", total / 3600, (total / 60) % 60, total % 60)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn format_elapsed_under_minute() {
|
|
assert_eq!(format_elapsed(std::time::Duration::from_secs(5)), "5s");
|
|
assert_eq!(format_elapsed(std::time::Duration::from_secs(59)), "59s");
|
|
}
|
|
|
|
#[test]
|
|
fn format_elapsed_under_hour() {
|
|
assert_eq!(format_elapsed(std::time::Duration::from_secs(60)), "1m 00s");
|
|
assert_eq!(format_elapsed(std::time::Duration::from_secs(125)), "2m 05s");
|
|
}
|
|
|
|
#[test]
|
|
fn format_elapsed_over_hour() {
|
|
assert_eq!(
|
|
format_elapsed(std::time::Duration::from_secs(3725)),
|
|
"1h 02m 05s"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn disabled_reporter_is_noop() {
|
|
let mut r = StatusReporter::new(false, 1, 3, "kf6-kio");
|
|
r.start();
|
|
r.phase("fetched");
|
|
r.phase("built");
|
|
r.done();
|
|
// No panic, no output, no state mutation when disabled.
|
|
assert_eq!(r.last_phase(), "");
|
|
}
|
|
|
|
#[test]
|
|
fn enabled_reporter_tracks_phases() {
|
|
let mut r = StatusReporter::new(true, 5, 15, "kf6-kio");
|
|
assert!(r.enabled);
|
|
r.start();
|
|
r.phase("fetched");
|
|
assert_eq!(r.last_phase(), "fetched");
|
|
r.phase("built");
|
|
assert_eq!(r.last_phase(), "built");
|
|
r.done();
|
|
assert_eq!(r.last_phase(), "done");
|
|
}
|
|
|
|
#[test]
|
|
fn status_reporter_enabled_logic() {
|
|
// All false: enabled (no TUI, no logs, stderr is a tty in tests... well, maybe not)
|
|
// In unit tests stderr is typically a tty capture. We just verify the
|
|
// boolean logic is correct for the in-process check.
|
|
assert!(!status_reporter_enabled(true, false)); // TUI on
|
|
assert!(!status_reporter_enabled(false, true)); // Logs on
|
|
}
|
|
}
|