consortium_cli/
display.rs1use std::collections::HashMap;
6use std::io::{self, Write};
7use std::sync::atomic::{AtomicUsize, Ordering};
8use std::sync::Arc;
9use std::time::Duration;
10
11use console::style;
12use indicatif::{ProgressBar, ProgressStyle};
13
14use consortium::worker::EventHandler;
15
16#[derive(Clone)]
20pub struct ProgressState {
21 completed: Arc<AtomicUsize>,
22 failed: Arc<AtomicUsize>,
23 total: usize,
24}
25
26impl ProgressState {
27 pub fn new(total: usize) -> Self {
28 Self {
29 completed: Arc::new(AtomicUsize::new(0)),
30 failed: Arc::new(AtomicUsize::new(0)),
31 total,
32 }
33 }
34
35 pub fn completed(&self) -> usize {
36 self.completed.load(Ordering::Relaxed)
37 }
38
39 pub fn failed(&self) -> usize {
40 self.failed.load(Ordering::Relaxed)
41 }
42
43 pub fn total(&self) -> usize {
44 self.total
45 }
46}
47
48pub struct ProgressHandler {
56 state: ProgressState,
57 bar: ProgressBar,
58}
59
60impl ProgressHandler {
61 pub fn new(state: ProgressState, bar: ProgressBar) -> Self {
62 Self { state, bar }
63 }
64}
65
66impl EventHandler for ProgressHandler {
67 fn on_close(&mut self, _node: &str, rc: i32) {
68 if rc != 0 {
69 self.state.failed.fetch_add(1, Ordering::Relaxed);
70 }
71 let done = self.state.completed.fetch_add(1, Ordering::Relaxed) + 1;
72 self.bar.set_position(done as u64);
73
74 let failed = self.state.failed();
75 if failed > 0 {
76 self.bar
77 .set_message(format!("({} failed)", style(failed).red()));
78 }
79 }
80
81 fn on_timeout(&mut self, _node: &str) {
82 self.state.failed.fetch_add(1, Ordering::Relaxed);
83 let done = self.state.completed.fetch_add(1, Ordering::Relaxed) + 1;
84 self.bar.set_position(done as u64);
85 self.bar
86 .set_message(format!("({} failed)", style(self.state.failed()).red()));
87 }
88}
89
90pub fn create_progress(total: usize) -> (ProgressBar, ProgressState, ProgressHandler) {
95 let state = ProgressState::new(total);
96 let bar = ProgressBar::new(total as u64);
97
98 bar.set_style(
99 ProgressStyle::with_template(
100 "{spinner:.green} [{bar:30.cyan/dim}] {pos}/{len} nodes {msg} ({elapsed})",
101 )
102 .unwrap()
103 .progress_chars("━╸─"),
104 );
105 bar.enable_steady_tick(Duration::from_millis(80));
106
107 let handler = ProgressHandler::new(state.clone(), bar.clone());
108 (bar, state, handler)
109}
110
111pub fn finish_progress(bar: &ProgressBar, state: &ProgressState, elapsed: Duration) {
113 let done = state.completed();
114 let failed = state.failed();
115 let ok = done - failed;
116 let total = state.total();
117
118 bar.finish_and_clear();
119
120 let elapsed_str = format!("{:.1}s", elapsed.as_secs_f64());
121
122 if failed == 0 {
123 eprintln!(
124 "{} {}/{} nodes completed in {}",
125 style("✓").green().bold(),
126 ok,
127 total,
128 style(elapsed_str).dim()
129 );
130 } else {
131 eprintln!(
132 "{} {}/{} ok, {} failed in {}",
133 style("✗").red().bold(),
134 ok,
135 total,
136 style(failed).red().bold(),
137 style(elapsed_str).dim()
138 );
139 }
140}
141
142pub fn print_gathered_header(nodes: &str, count: usize, out: &mut impl Write) -> io::Result<()> {
157 let sep = "-".repeat(15);
158 writeln!(out, "{sep}")?;
159 if count > 1 {
160 writeln!(out, "{nodes} ({count})")?;
161 } else {
162 writeln!(out, "{nodes}")?;
163 }
164 writeln!(out, "{sep}")?;
165 Ok(())
166}
167
168pub fn print_line_with_label(node: &str, line: &str, out: &mut impl Write) -> io::Result<()> {
170 writeln!(out, "{node}: {line}")
171}
172
173pub struct LineStreamHandler {
199 label: bool,
200 partial: HashMap<String, Vec<u8>>,
201 inner: Option<Box<dyn EventHandler>>,
202}
203
204impl LineStreamHandler {
205 pub fn new(label: bool) -> Self {
207 Self {
208 label,
209 partial: HashMap::new(),
210 inner: None,
211 }
212 }
213
214 pub fn with_inner(mut self, inner: Box<dyn EventHandler>) -> Self {
216 self.inner = Some(inner);
217 self
218 }
219
220 fn emit_line(&self, node: &str, bytes: &[u8]) {
222 let text = String::from_utf8_lossy(bytes);
223 let mut out = io::stdout().lock();
224 if self.label {
225 let _ = writeln!(out, "{node}: {text}");
226 } else {
227 let _ = writeln!(out, "{text}");
228 }
229 let _ = out.flush();
232 }
233
234 fn feed(&self, node: &str, buf: &mut Vec<u8>, chunk: &[u8]) {
236 buf.extend_from_slice(chunk);
237 while let Some(pos) = buf.iter().position(|b| *b == b'\n') {
238 let line: Vec<u8> = buf.drain(..pos).collect();
239 let _ = buf.drain(..1); self.emit_line(node, &line);
241 }
242 }
243
244 fn flush_partial(&mut self, node: &str) {
246 if let Some(buf) = self.partial.remove(node) {
247 if !buf.is_empty() {
248 self.emit_line(node, &buf);
249 }
250 }
251 }
252}
253
254impl EventHandler for LineStreamHandler {
255 fn on_start(&mut self, worker: &dyn consortium::worker::Worker) {
256 if let Some(ref mut h) = self.inner {
257 h.on_start(worker);
258 }
259 }
260
261 fn on_read(&mut self, node: &str, fd: std::os::unix::io::RawFd, msg: &[u8]) {
262 let _ = fd; let mut buf = self.partial.remove(node).unwrap_or_default();
264 self.feed(node, &mut buf, msg);
265 self.partial.insert(node.to_string(), buf);
266 if let Some(ref mut h) = self.inner {
267 h.on_read(node, fd, msg);
268 }
269 }
270
271 fn on_close(&mut self, node: &str, rc: i32) {
272 self.flush_partial(node);
273 if let Some(ref mut h) = self.inner {
274 h.on_close(node, rc);
275 }
276 }
277
278 fn on_timeout(&mut self, node: &str) {
279 self.flush_partial(node);
280 if let Some(ref mut h) = self.inner {
281 h.on_timeout(node);
282 }
283 }
284
285 fn on_error(&mut self, node: &str, error: &consortium::worker::WorkerError) {
286 if let Some(ref mut h) = self.inner {
287 h.on_error(node, error);
288 }
289 }
290}