Skip to main content

consortium_cli/
display.rs

1//! Shared output formatting for consortium CLI tools.
2//!
3//! Provides nh-inspired progress reporting and output formatting.
4
5use std::io::{self, Write};
6use std::sync::atomic::{AtomicUsize, Ordering};
7use std::sync::Arc;
8use std::time::Duration;
9
10use console::style;
11use indicatif::{ProgressBar, ProgressStyle};
12
13use consortium::worker::EventHandler;
14
15// ── Progress tracking ───────────────────────────────────────────────────────
16
17/// Tracks completed/failed node counts. Shared between ProgressHandler and caller.
18#[derive(Clone)]
19pub struct ProgressState {
20    completed: Arc<AtomicUsize>,
21    failed: Arc<AtomicUsize>,
22    total: usize,
23}
24
25impl ProgressState {
26    pub fn new(total: usize) -> Self {
27        Self {
28            completed: Arc::new(AtomicUsize::new(0)),
29            failed: Arc::new(AtomicUsize::new(0)),
30            total,
31        }
32    }
33
34    pub fn completed(&self) -> usize {
35        self.completed.load(Ordering::Relaxed)
36    }
37
38    pub fn failed(&self) -> usize {
39        self.failed.load(Ordering::Relaxed)
40    }
41
42    pub fn total(&self) -> usize {
43        self.total
44    }
45}
46
47/// EventHandler that drives an indicatif ProgressBar on each node completion.
48///
49/// Pass as `user_handler` to `task.schedule()`. The TaskGatheringHandler
50/// chains `on_close`/`on_timeout` to us — no core changes needed.
51///
52/// ProgressBar is internally Arc'd, so updates from the poll thread are
53/// immediately visible to the steady-tick render thread.
54pub struct ProgressHandler {
55    state: ProgressState,
56    bar: ProgressBar,
57}
58
59impl ProgressHandler {
60    pub fn new(state: ProgressState, bar: ProgressBar) -> Self {
61        Self { state, bar }
62    }
63}
64
65impl EventHandler for ProgressHandler {
66    fn on_close(&mut self, _node: &str, rc: i32) {
67        if rc != 0 {
68            self.state.failed.fetch_add(1, Ordering::Relaxed);
69        }
70        let done = self.state.completed.fetch_add(1, Ordering::Relaxed) + 1;
71        self.bar.set_position(done as u64);
72
73        let failed = self.state.failed();
74        if failed > 0 {
75            self.bar
76                .set_message(format!("({} failed)", style(failed).red()));
77        }
78    }
79
80    fn on_timeout(&mut self, _node: &str) {
81        self.state.failed.fetch_add(1, Ordering::Relaxed);
82        let done = self.state.completed.fetch_add(1, Ordering::Relaxed) + 1;
83        self.bar.set_position(done as u64);
84        self.bar
85            .set_message(format!("({} failed)", style(self.state.failed()).red()));
86    }
87}
88
89/// Create an nh-style progress bar + state for cluster operations.
90///
91/// Returns (ProgressBar, ProgressState, ProgressHandler). Pass the handler
92/// to `task.schedule(..., Some(Box::new(handler)), ...)`.
93pub fn create_progress(total: usize) -> (ProgressBar, ProgressState, ProgressHandler) {
94    let state = ProgressState::new(total);
95    let bar = ProgressBar::new(total as u64);
96
97    bar.set_style(
98        ProgressStyle::with_template(
99            "{spinner:.green} [{bar:30.cyan/dim}] {pos}/{len} nodes {msg} ({elapsed})",
100        )
101        .unwrap()
102        .progress_chars("━╸─"),
103    );
104    bar.enable_steady_tick(Duration::from_millis(80));
105
106    let handler = ProgressHandler::new(state.clone(), bar.clone());
107    (bar, state, handler)
108}
109
110/// Finish the progress bar with a summary line.
111pub fn finish_progress(bar: &ProgressBar, state: &ProgressState, elapsed: Duration) {
112    let done = state.completed();
113    let failed = state.failed();
114    let ok = done - failed;
115    let total = state.total();
116
117    bar.finish_and_clear();
118
119    let elapsed_str = format!("{:.1}s", elapsed.as_secs_f64());
120
121    if failed == 0 {
122        eprintln!(
123            "{} {}/{} nodes completed in {}",
124            style("✓").green().bold(),
125            ok,
126            total,
127            style(elapsed_str).dim()
128        );
129    } else {
130        eprintln!(
131            "{} {}/{} ok, {} failed in {}",
132            style("✗").red().bold(),
133            ok,
134            total,
135            style(failed).red().bold(),
136            style(elapsed_str).dim()
137        );
138    }
139}
140
141// ── Output formatting ───────────────────────────────────────────────────────
142
143/// Print gathered output with a node header.
144///
145/// ```text
146/// ---------------
147/// node[1-3]
148/// ---------------
149/// hello world
150/// ```
151pub fn print_gathered_header(nodes: &str, out: &mut impl Write) -> io::Result<()> {
152    let sep = "-".repeat(15);
153    writeln!(out, "{sep}")?;
154    writeln!(out, "{nodes}")?;
155    writeln!(out, "{sep}")?;
156    Ok(())
157}
158
159/// Print a single line with node prefix: `node1: output`
160pub fn print_line_with_label(node: &str, line: &str, out: &mut impl Write) -> io::Result<()> {
161    writeln!(out, "{node}: {line}")
162}