1use std::{
3 collections::HashSet,
4 io::{self, Write},
5};
6
7use crossterm::{
8 cursor,
9 execute,
10 style::{Color, ResetColor, SetForegroundColor},
11};
12
13use crate::{
14 icons::Icons,
15 state::{BuildStatus, DerivationId, State, current_time},
16 types::{LegendStyle, SummaryStyle},
17};
18
19#[must_use]
21pub fn format_duration(secs: f64) -> String {
22 if secs < 60.0 {
23 format!("{secs:.0}s")
24 } else if secs < 3600.0 {
25 format!("{:.0}m{:.0}s", secs / 60.0, secs % 60.0)
26 } else {
27 format!("{:.0}h{:.0}m", secs / 3600.0, (secs % 3600.0) / 60.0)
28 }
29}
30
31pub struct DisplayConfig {
32 pub show_timers: bool,
33 pub max_tree_depth: usize,
34 pub max_visible_lines: usize,
35 pub use_color: bool,
36 pub format: crate::types::DisplayFormat,
37 pub legend_style: LegendStyle,
38 pub summary_style: SummaryStyle,
39 pub icons: &'static Icons,
40}
41
42impl Default for DisplayConfig {
43 fn default() -> Self {
44 Self {
45 show_timers: true,
46 max_tree_depth: 10,
47 max_visible_lines: 100,
48 use_color: true,
49 format: crate::types::DisplayFormat::Tree,
50 legend_style: LegendStyle::Table,
51 summary_style: SummaryStyle::Concise,
52 icons: crate::icons::detect(),
53 }
54 }
55}
56
57pub struct Display<W: Write> {
58 writer: W,
59 config: DisplayConfig,
60 last_lines: usize,
62 printed_log_lines: usize,
64}
65
66struct TreeNode {
67 drv_id: DerivationId,
68 children: Vec<Self>,
69}
70
71impl<W: Write> Display<W> {
72 pub const fn new(writer: W, config: DisplayConfig) -> io::Result<Self> {
73 Ok(Self {
74 writer,
75 config,
76 last_lines: 0,
77 printed_log_lines: 0,
78 })
79 }
80
81 pub fn clear_previous(&mut self) -> io::Result<()> {
82 if self.last_lines > 0 {
83 execute!(
87 self.writer,
88 cursor::MoveToColumn(0),
89 cursor::MoveUp(self.last_lines as u16),
90 cursor::MoveToColumn(0),
91 crossterm::terminal::Clear(
92 crossterm::terminal::ClearType::FromCursorDown
93 )
94 )?;
95 }
96 Ok(())
97 }
98
99 pub fn render(&mut self, state: &State, logs: &[String]) -> io::Result<()> {
100 let new_logs = &logs[self.printed_log_lines.min(logs.len())..];
103 if !new_logs.is_empty() {
104 self.clear_previous()?;
106 let mut log_out = String::with_capacity(new_logs.len() * 80);
107 for line in new_logs {
108 log_out.push_str(line);
109 log_out.push('\n');
110 }
111 self.writer.write_all(log_out.as_bytes())?;
112 self.printed_log_lines = logs.len();
113 self.last_lines = 0; }
115
116 self.clear_previous()?;
118
119 let mut graph_lines = match self.config.format {
121 crate::types::DisplayFormat::Tree => {
122 let tree_lines = self.render_tree_view(state);
123 let has_tree = !tree_lines.is_empty();
124 let mut g = tree_lines;
125 g.extend(self.render_legend(state, has_tree));
126 g
127 },
128 crate::types::DisplayFormat::Plain => self.render_plain_view(state),
129 crate::types::DisplayFormat::Dashboard => {
130 self.render_dashboard_view(state)
131 },
132 };
133
134 if graph_lines.len() > self.config.max_visible_lines {
135 graph_lines.truncate(self.config.max_visible_lines);
136 }
137
138 self.last_lines = graph_lines.len();
139
140 let mut out = String::with_capacity(graph_lines.len() * 80);
141 for line in &graph_lines {
142 out.push_str(line);
143 out.push('\n');
144 }
145 self.writer.write_all(out.as_bytes())?;
146 self.writer.flush()
147 }
148
149 pub fn render_final(&mut self, state: &State) -> io::Result<()> {
150 tracing::debug!("render_final called");
151
152 self.clear_previous()?;
154
155 let mut lines = Vec::new();
156
157 match self.config.format {
159 crate::types::DisplayFormat::Tree => {
160 let tree_lines = self.render_tree_view(state);
163 lines.extend(tree_lines);
164 lines.extend(self.render_final_summary(state));
165 },
166 crate::types::DisplayFormat::Plain => {
167 lines.extend(self.render_plain_view(state));
168 lines.extend(self.render_final_summary(state));
169 },
170 crate::types::DisplayFormat::Dashboard => {
171 lines.extend(self.render_dashboard_final(state));
172 },
173 }
174
175 tracing::debug!("render_final: {} lines to print", lines.len());
176
177 for line in lines {
179 writeln!(self.writer, "{line}")?;
180 }
181
182 writeln!(self.writer)?;
183 self.writer.flush()?;
184
185 Ok(())
186 }
187
188 fn render_final_summary(&self, state: &State) -> Vec<String> {
189 match self.config.summary_style {
190 SummaryStyle::Concise => self.render_finished_line(state),
191 SummaryStyle::Table => self.render_table_summary(state),
192 SummaryStyle::Full => self.render_full_summary(state),
193 }
194 }
195
196 fn render_finished_line(&self, state: &State) -> Vec<String> {
198 let failed = state.full_summary.failed_builds.len();
199 let completed = state.full_summary.completed_builds.len();
200 let nix_errors = state.nix_errors.len();
201 let duration = current_time() - state.start_time;
202 let now = chrono::Local::now();
203 let at = now.format("%H:%M:%S");
204 let dur = self.format_duration(duration);
205
206 let ic = self.ic();
207 let line = if failed > 0 {
208 let noun = if failed == 1 { "failure" } else { "failures" };
209 format!(
210 "{} {} at {} after {}",
211 self.colored(ic.failed, Color::DarkRed),
212 self.colored(
213 &format!("Exited after {failed} build {noun}"),
214 Color::DarkRed
215 ),
216 self.colored(&at.to_string(), Color::DarkRed),
217 self.colored(&dur, Color::DarkRed),
218 )
219 } else if nix_errors > 0 {
220 let noun = if nix_errors == 1 { "error" } else { "errors" };
221 format!(
222 "{} {} at {} after {}",
223 self.colored(ic.failed, Color::DarkRed),
224 self.colored(
225 &format!("Exited with {nix_errors} nix {noun}"),
226 Color::DarkRed
227 ),
228 self.colored(&at.to_string(), Color::DarkRed),
229 self.colored(&dur, Color::DarkRed),
230 )
231 } else {
232 let mut s = format!(
233 "{} after {}",
234 self.colored(&format!("Finished at {at}"), Color::DarkGreen),
235 self.colored(&dur, Color::DarkGreen),
236 );
237 if completed > 0 {
238 s.push_str(&format!(
239 " {} {completed}",
240 self.colored(ic.done, Color::DarkGreen)
241 ));
242 }
243 s
244 };
245
246 vec![line]
247 }
248
249 fn render_table_summary(&self, state: &State) -> Vec<String> {
250 let completed = state.full_summary.completed_builds.len();
251 let failed = state.full_summary.failed_builds.len();
252 let dl_done = state.full_summary.completed_downloads.len();
253 let ul_done = state.full_summary.completed_uploads.len();
254 let duration = current_time() - state.start_time;
255 let now = chrono::Local::now();
256 let at = now.format("%H:%M:%S");
257 let dur = self.format_duration(duration);
258
259 if completed + failed + dl_done + ul_done == 0 {
260 return self.render_finished_line(state);
261 }
262
263 let mut host_map: std::collections::HashMap<String, (usize, usize)> =
265 std::collections::HashMap::new();
266 for b in state.full_summary.completed_builds.values() {
267 host_map.entry(b.host.name().to_string()).or_default().0 += 1;
268 }
269 for b in state.full_summary.failed_builds.values() {
270 host_map.entry(b.host.name().to_string()).or_default().1 += 1;
271 }
272 let many_hosts = host_map.len() > 1;
273
274 let mut lines = Vec::new();
275
276 let mut hdr_parts = Vec::new();
278 if completed + failed > 0 {
279 hdr_parts.push("Builds");
280 }
281 if dl_done > 0 {
282 hdr_parts.push("Downloads");
283 }
284 if ul_done > 0 {
285 hdr_parts.push("Uploads");
286 }
287 let ic = self.ic();
288 lines.push(format!(
289 "{} {}",
290 self.colored("┏━━━", Color::DarkBlue),
291 hdr_parts.join(" ")
292 ));
293
294 if many_hosts {
296 let mut hosts: Vec<_> = host_map.keys().cloned().collect();
297 hosts.sort();
298 for host in &hosts {
299 let (done, fail) = host_map[host];
300 let mut parts = Vec::new();
301 if done > 0 {
302 parts.push(format!(
303 "{} {done}",
304 self.colored(ic.done, Color::DarkGreen)
305 ));
306 }
307 if fail > 0 {
308 parts.push(format!(
309 "{} {fail}",
310 self.colored(ic.failed, Color::DarkRed)
311 ));
312 }
313 lines.push(format!(
314 "{} {} {}",
315 self.colored("┃", Color::DarkBlue),
316 parts.join(" "),
317 self.colored(host, Color::DarkMagenta),
318 ));
319 }
320 }
321
322 let mut sum_parts = Vec::new();
324 if completed > 0 {
325 sum_parts.push(format!(
326 "{} {completed}",
327 self.colored(ic.done, Color::DarkGreen)
328 ));
329 }
330 if failed > 0 {
331 sum_parts.push(format!(
332 "{} {failed}",
333 self.colored(ic.failed, Color::DarkRed)
334 ));
335 }
336 if dl_done > 0 {
337 sum_parts.push(format!(
338 "{} {dl_done}",
339 self.colored(ic.download, Color::DarkGreen)
340 ));
341 }
342 if ul_done > 0 {
343 sum_parts.push(format!(
344 "{} {ul_done}",
345 self.colored(ic.upload, Color::DarkGreen)
346 ));
347 }
348
349 let finish = if failed > 0 || !state.nix_errors.is_empty() {
350 self.colored(&format!("Exited at {at} after {dur}"), Color::DarkRed)
351 } else {
352 self.colored(&format!("Finished at {at} after {dur}"), Color::DarkGreen)
353 };
354 sum_parts.push(finish);
355
356 lines.push(format!(
357 "{} ∑ {}",
358 self.colored("┗━", Color::DarkBlue),
359 sum_parts.join(" │ ")
360 ));
361
362 lines
363 }
364
365 fn render_full_summary(&self, state: &State) -> Vec<String> {
366 let completed = state.full_summary.completed_builds.len();
367 let failed = state.full_summary.failed_builds.len();
368 let dl_done = state.full_summary.completed_downloads.len();
369 let dl_running = state.full_summary.running_downloads.len();
370 let ul_done = state.full_summary.completed_uploads.len();
371 let ul_running = state.full_summary.running_uploads.len();
372 let duration = current_time() - state.start_time;
373 let now = chrono::Local::now();
374 let at = now.format("%H:%M:%S");
375
376 let v = self.colored("┃", Color::DarkBlue);
377
378 let mut lines = Vec::new();
379 lines.push(format!(
380 "{} Build Summary",
381 self.colored("┏━━━", Color::DarkBlue)
382 ));
383
384 let ic = self.ic();
385 if completed > 0 || failed > 0 {
386 let mut bp = Vec::new();
387 if completed > 0 {
388 bp.push(format!(
389 "{} {completed} built",
390 self.colored(ic.done, Color::DarkGreen)
391 ));
392 }
393 if failed > 0 {
394 bp.push(format!(
395 "{} {failed} failed",
396 self.colored(ic.failed, Color::DarkRed)
397 ));
398 }
399 lines.push(format!("{} Builds: {}", v, bp.join(" ")));
400 }
401
402 let total_dl = dl_done + dl_running;
403 let total_ul = ul_done + ul_running;
404 if total_dl > 0 {
405 lines.push(format!(
406 "{} Downloads: {} fetched",
407 v,
408 self.colored(&total_dl.to_string(), Color::DarkGreen)
409 ));
410 }
411 if total_ul > 0 {
412 lines.push(format!(
413 "{} Uploads: {} pushed",
414 v,
415 self.colored(&total_ul.to_string(), Color::DarkGreen)
416 ));
417 }
418
419 if !state.nix_errors.is_empty() {
420 lines.push(format!(
421 "{} {} {} nix error(s)",
422 v,
423 self.colored(ic.failed, Color::DarkRed),
424 state.nix_errors.len()
425 ));
426 }
427
428 let finish_label = if failed > 0 || !state.nix_errors.is_empty() {
429 self.colored(&format!("Exited at {at}"), Color::DarkRed)
430 } else {
431 self.colored(&format!("Finished at {at}"), Color::DarkGreen)
432 };
433 lines.push(format!(
434 "{} {} after {}",
435 self.colored("┗━", Color::DarkBlue),
436 finish_label,
437 self.colored(&self.format_duration(duration), Color::DarkGrey),
438 ));
439
440 lines
441 }
442
443 fn render_legend(&self, state: &State, has_tree: bool) -> Vec<String> {
444 match self.config.legend_style {
445 LegendStyle::Compact => self.render_compact_legend(state),
446 LegendStyle::Table => self.render_table_legend(state, has_tree),
447 LegendStyle::Verbose => self.render_verbose_legend(state, has_tree),
448 }
449 }
450
451 fn render_compact_legend(&self, state: &State) -> Vec<String> {
452 let running = state.full_summary.running_builds.len();
453 let completed = state.full_summary.completed_builds.len();
454 let failed = state.full_summary.failed_builds.len();
455 let planned = state.full_summary.planned_builds.len();
456 let dl = state.full_summary.running_downloads.len();
457 let ul = state.full_summary.running_uploads.len();
458
459 if running + completed + failed + planned + dl + ul == 0 {
460 return vec![];
461 }
462
463 let duration = current_time() - state.start_time;
464 let ic = self.ic();
465
466 let mut parts: Vec<String> = Vec::new();
468 parts.push(self.count_colored(ic.running, running, Color::DarkYellow));
469 parts.push(self.count_colored(ic.done, completed, Color::DarkGreen));
470 parts.push(self.count_colored(ic.failed, failed, Color::DarkRed));
471 parts.push(self.count_colored(ic.planned, planned, Color::DarkBlue));
472 if dl > 0 {
473 parts.push(format!(
474 "{} {dl}",
475 self.colored(ic.download, Color::DarkYellow)
476 ));
477 }
478 if ul > 0 {
479 parts.push(format!(
480 "{} {ul}",
481 self.colored(ic.upload, Color::DarkYellow)
482 ));
483 }
484 parts.push(format!(
485 "{} {}",
486 self.colored(ic.clock, Color::DarkGrey),
487 self.colored(&self.format_duration(duration), Color::DarkGrey),
488 ));
489
490 vec![format!(
491 "{} {}",
492 self.colored("┗━", Color::DarkBlue),
493 parts.join(" │ ")
494 )]
495 }
496
497 fn render_table_legend(&self, state: &State, has_tree: bool) -> Vec<String> {
498 let running = state.full_summary.running_builds.len();
499 let completed = state.full_summary.completed_builds.len();
500 let failed = state.full_summary.failed_builds.len();
501 let planned = state.full_summary.planned_builds.len();
502 let dl_running = state.full_summary.running_downloads.len();
503 let dl_done = state.full_summary.completed_downloads.len();
504 let ul_running = state.full_summary.running_uploads.len();
505 let ul_done = state.full_summary.completed_uploads.len();
506
507 let show_builds = running + completed + failed + planned > 0;
508 let show_dl = dl_running + dl_done > 0;
509 let show_ul = ul_running + ul_done > 0;
510
511 if !show_builds && !show_dl && !show_ul {
512 return vec![];
513 }
514
515 let now = current_time();
516 let duration = now - state.start_time;
517 let v = self.colored("┃", Color::DarkBlue);
518
519 let mut header_parts: Vec<&str> = Vec::new();
521 if show_builds {
522 header_parts.push("Builds");
523 }
524 if show_dl {
525 header_parts.push("Downloads");
526 }
527 if show_ul {
528 header_parts.push("Uploads");
529 }
530
531 let mut lines = Vec::new();
532
533 let header_prefix = if has_tree {
535 "┣━━━"
536 } else {
537 "┏━━━"
538 };
539 lines.push(format!(
540 "{} {}",
541 self.colored(header_prefix, Color::DarkBlue),
542 header_parts.join(" ")
543 ));
544
545 let mut running_entries: Vec<(String, f64, String)> = state
547 .full_summary
548 .running_builds
549 .iter()
550 .filter_map(|(drv_id, build)| {
551 let info = state.get_derivation_info(*drv_id)?;
552 let elapsed = now - build.start;
553 let host_label = match &build.host {
554 cognos::Host::Remote(h) => {
555 format!(" on {}", self.colored(h, Color::DarkMagenta))
556 },
557 _ => String::new(),
558 };
559 Some((info.name.name.clone(), elapsed, host_label))
560 })
561 .collect();
562 running_entries.sort_by(|a, b| {
564 b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)
565 });
566
567 let dl_name_width = state
568 .full_summary
569 .running_downloads
570 .keys()
571 .filter_map(|id| {
572 state.store_path_infos.get(id).map(|pi| pi.name.name.len())
573 })
574 .max()
575 .unwrap_or(0);
576
577 let name_width = running_entries
578 .iter()
579 .map(|(n, ..)| n.len())
580 .chain(std::iter::once(dl_name_width))
581 .max()
582 .unwrap_or(0)
583 .min(48);
584
585 if !has_tree {
589 let ic = self.ic();
590 for (name, elapsed, host_label) in &running_entries {
591 lines.push(format!(
592 "{} {} {:<width$} {} {}{}",
593 v,
594 self.colored(ic.running, Color::DarkYellow),
595 self.truncate_name(name, name_width),
596 self.colored(ic.clock, Color::DarkGrey),
597 self.colored(&self.format_duration(*elapsed), Color::DarkGrey),
598 host_label,
599 width = name_width,
600 ));
601 }
602
603 for (path_id, transfer) in &state.full_summary.running_downloads {
605 if let Some(pi) = state.store_path_infos.get(path_id) {
606 let elapsed = now - transfer.start;
607 let size_str = if let Some(total) = transfer.total_bytes {
608 self.format_bytes(transfer.bytes_transferred, total)
609 } else {
610 format!("{} B", transfer.bytes_transferred)
611 };
612 lines.push(format!(
613 "{} {} {:<width$} {} {} {}",
614 v,
615 self.colored(ic.download, Color::DarkYellow),
616 self.truncate_name(&pi.name.name, name_width),
617 self.colored(&size_str, Color::DarkGrey),
618 self.colored(ic.clock, Color::DarkGrey),
619 self.colored(&self.format_duration(elapsed), Color::DarkGrey),
620 width = name_width,
621 ));
622 }
623 }
624
625 for (path_id, transfer) in &state.full_summary.running_uploads {
627 if let Some(pi) = state.store_path_infos.get(path_id) {
628 let elapsed = now - transfer.start;
629 lines.push(format!(
630 "{} {} {:<width$} {} {}",
631 v,
632 self.colored(ic.upload, Color::DarkYellow),
633 self.truncate_name(&pi.name.name, name_width),
634 self.colored(ic.clock, Color::DarkGrey),
635 self.colored(&self.format_duration(elapsed), Color::DarkGrey),
636 width = name_width,
637 ));
638 }
639 }
640 }
641
642 let ic = self.ic();
645 let mut sum_parts: Vec<String> = Vec::new();
646 if show_builds {
647 sum_parts.push(self.count_colored(
648 ic.running,
649 running,
650 Color::DarkYellow,
651 ));
652 sum_parts.push(self.count_colored(ic.done, completed, Color::DarkGreen));
653 sum_parts.push(self.count_colored(ic.failed, failed, Color::DarkRed));
654 sum_parts.push(self.count_colored(ic.planned, planned, Color::DarkBlue));
655 }
656 if show_dl {
657 if dl_running > 0 || dl_done > 0 {
659 sum_parts.push(format!(
660 "{} {}",
661 self.colored(ic.download, Color::DarkGrey),
662 [
663 (dl_running > 0).then(|| {
664 self.count_colored(ic.running, dl_running, Color::DarkYellow)
665 }),
666 (dl_done > 0)
667 .then(|| self.count_colored(ic.done, dl_done, Color::DarkGreen)),
668 ]
669 .into_iter()
670 .flatten()
671 .collect::<Vec<_>>()
672 .join(" "),
673 ));
674 }
675 }
676 if show_ul && (ul_running > 0 || ul_done > 0) {
677 sum_parts.push(format!(
678 "{} {}",
679 self.colored(ic.upload, Color::DarkGrey),
680 [
681 (ul_running > 0).then(|| {
682 self.count_colored(ic.running, ul_running, Color::DarkYellow)
683 }),
684 (ul_done > 0)
685 .then(|| self.count_colored(ic.done, ul_done, Color::DarkGreen)),
686 ]
687 .into_iter()
688 .flatten()
689 .collect::<Vec<_>>()
690 .join(" "),
691 ));
692 }
693 sum_parts.push(format!(
695 "{} {}",
696 self.colored(ic.clock, Color::DarkGrey),
697 self.colored(&self.format_duration(duration), Color::DarkGrey),
698 ));
699
700 lines.push(format!(
702 "{} {} {}",
703 self.colored("┗━", Color::DarkBlue),
704 self.colored(ic.summary, Color::DarkGrey),
705 sum_parts.join(" │ ")
706 ));
707
708 lines
709 }
710
711 fn render_verbose_legend(
712 &self,
713 state: &State,
714 has_tree: bool,
715 ) -> Vec<String> {
716 let running = state.full_summary.running_builds.len();
717 let completed = state.full_summary.completed_builds.len();
718 let failed = state.full_summary.failed_builds.len();
719 let planned = state.full_summary.planned_builds.len();
720 let dl_running = state.full_summary.running_downloads.len();
721 let ul_running = state.full_summary.running_uploads.len();
722
723 if running + completed + failed + planned + dl_running + ul_running == 0 {
724 return vec![];
725 }
726
727 let now = current_time();
728 let duration = now - state.start_time;
729 let prefix = if has_tree {
730 "┣━━━"
731 } else {
732 "┏━━━"
733 };
734 let v = self.colored("┃", Color::DarkBlue);
735
736 let mut lines = Vec::new();
737 lines.push(format!(
738 "{} Build Summary:",
739 self.colored(prefix, Color::DarkBlue)
740 ));
741
742 let mut running_entries: Vec<(String, String, String)> = state
744 .full_summary
745 .running_builds
746 .iter()
747 .filter_map(|(drv_id, build)| {
748 let info = state.get_derivation_info(*drv_id)?;
749 let elapsed = now - build.start;
750 let host = match &build.host {
751 cognos::Host::Localhost => String::new(),
752 cognos::Host::Remote(h) => {
753 format!(" {}", self.colored(h, Color::DarkMagenta))
754 },
755 };
756 Some((info.name.name.clone(), self.format_duration(elapsed), host))
757 })
758 .collect();
759 running_entries.sort_by(|a, b| a.0.cmp(&b.0));
760
761 let name_width = running_entries
762 .iter()
763 .map(|(n, ..)| n.len())
764 .max()
765 .unwrap_or(0)
766 .min(48);
767
768 let ic = self.ic();
769 for (name, elapsed, host) in &running_entries {
770 lines.push(format!(
771 "{} {} {:<width$} {}{}",
772 v,
773 self.colored(ic.running, Color::DarkYellow),
774 self.truncate_name(name, name_width),
775 self.colored(elapsed, Color::DarkGrey),
776 host,
777 width = name_width,
778 ));
779 }
780
781 for (path_id, transfer) in &state.full_summary.running_downloads {
783 if let Some(pi) = state.store_path_infos.get(path_id) {
784 let elapsed = now - transfer.start;
785 let size = if let Some(total) = transfer.total_bytes {
786 self.format_bytes(transfer.bytes_transferred, total)
787 } else {
788 format!("{} B", transfer.bytes_transferred)
789 };
790 lines.push(format!(
791 "{} {} {:<width$} {} {}",
792 v,
793 self.colored(ic.download, Color::DarkYellow),
794 self.truncate_name(&pi.name.name, name_width),
795 self.colored(&size, Color::DarkGrey),
796 self.colored(&self.format_duration(elapsed), Color::DarkGrey),
797 width = name_width,
798 ));
799 }
800 }
801
802 let ic = self.ic();
803 let mut sum_parts: Vec<String> = Vec::new();
804 sum_parts.push(format!(
805 "{} {running} running",
806 self.colored(ic.running, Color::DarkYellow)
807 ));
808 sum_parts.push(format!(
809 "{} {completed} completed",
810 self.colored(ic.done, Color::DarkGreen)
811 ));
812 sum_parts.push(format!(
813 "{} {failed} failed",
814 self.colored(ic.failed, Color::DarkRed)
815 ));
816 sum_parts.push(format!(
817 "{} {planned} planned",
818 self.colored(ic.planned, Color::DarkBlue)
819 ));
820 if dl_running > 0 {
821 sum_parts.push(format!(
822 "{} {dl_running} downloading",
823 self.colored(ic.download, Color::DarkYellow)
824 ));
825 }
826 if ul_running > 0 {
827 sum_parts.push(format!(
828 "{} {ul_running} uploading",
829 self.colored(ic.upload, Color::DarkYellow)
830 ));
831 }
832 sum_parts.push(format!(
833 "{} {}",
834 self.colored(ic.clock, Color::DarkGrey),
835 self.colored(&self.format_duration(duration), Color::DarkGrey),
836 ));
837
838 lines.push(format!(
839 "{} {}",
840 self.colored("┗━", Color::DarkBlue),
841 sum_parts.join(" │ ")
842 ));
843
844 lines
845 }
846
847 fn render_plain_view(&self, state: &State) -> Vec<String> {
848 let now = current_time();
849 let duration = now - state.start_time;
850 let running = state.full_summary.running_builds.len();
851 let planned = state.full_summary.planned_builds.len();
852 let completed = state.full_summary.completed_builds.len();
853 let downloading = state.full_summary.running_downloads.len();
854 let uploading = state.full_summary.running_uploads.len();
855
856 if running + planned + completed + downloading + uploading == 0 {
857 return vec![];
858 }
859
860 let mut lines = Vec::new();
861
862 let mut builds: Vec<_> = state
864 .full_summary
865 .running_builds
866 .iter()
867 .filter_map(|(drv_id, build)| {
868 let info = state.get_derivation_info(*drv_id)?;
869 Some((info.name.name.clone(), build.clone()))
870 })
871 .collect();
872 builds.sort_by(|a, b| a.0.cmp(&b.0));
873
874 let ic = self.ic();
875
876 let mut header_parts: Vec<String> = Vec::new();
877 if planned > 0 {
878 header_parts.push(format!(
879 "{} {planned} planned",
880 self.colored(ic.planned, Color::DarkBlue)
881 ));
882 }
883 if downloading > 0 {
884 header_parts.push(format!(
885 "{} {downloading} downloading",
886 self.colored(ic.download, Color::DarkYellow)
887 ));
888 }
889 if uploading > 0 {
890 header_parts.push(format!(
891 "{} {uploading} uploading",
892 self.colored(ic.upload, Color::DarkYellow)
893 ));
894 }
895 let duration_str = self.format_duration(duration);
896 let header = if header_parts.is_empty() {
897 format!(
898 "{} {} {}",
899 self.colored("━", Color::DarkBlue),
900 self.colored(ic.clock, Color::DarkGrey),
901 self.colored(&duration_str, Color::DarkGrey),
902 )
903 } else {
904 format!(
905 "{} {} {} {}",
906 self.colored("━", Color::DarkBlue),
907 self.colored(ic.clock, Color::DarkGrey),
908 header_parts.join(" "),
909 self.colored(&duration_str, Color::DarkGrey),
910 )
911 };
912 lines.push(header);
913
914 for (name, build) in &builds {
915 let elapsed = now - build.start;
916 let mut suffix = String::new();
917 if let Some(est) = build.estimate {
918 let remaining = est.saturating_sub(elapsed as u64);
919 suffix = format!(
920 " {} {}",
921 self.colored(ic.estimate, Color::DarkGrey),
922 self
923 .colored(&self.format_duration(remaining as f64), Color::DarkGrey)
924 );
925 }
926 let host_label = match &build.host {
927 cognos::Host::Remote(h) => {
928 format!(" {}", self.colored(h, Color::DarkMagenta))
929 },
930 _ => String::new(),
931 };
932 lines.push(format!(
933 " {} {} {}{}{}",
934 self.colored(ic.running, Color::DarkYellow),
935 name,
936 self.colored(&self.format_duration(elapsed), Color::DarkGrey),
937 suffix,
938 host_label,
939 ));
940 }
941
942 for (path_id, transfer) in &state.full_summary.running_downloads {
944 if let Some(pi) = state.store_path_infos.get(path_id) {
945 let size = if let Some(total) = transfer.total_bytes {
946 self.format_bytes(transfer.bytes_transferred, total)
947 } else {
948 format!("{} B", transfer.bytes_transferred)
949 };
950 lines.push(format!(
951 " {} {} {}",
952 self.colored(ic.download, Color::DarkYellow),
953 pi.name.name,
954 self.colored(&size, Color::DarkGrey),
955 ));
956 }
957 }
958
959 for (path_id, transfer) in &state.full_summary.running_uploads {
961 if let Some(pi) = state.store_path_infos.get(path_id) {
962 let size = if let Some(total) = transfer.total_bytes {
963 self.format_bytes(transfer.bytes_transferred, total)
964 } else {
965 format!("{} B", transfer.bytes_transferred)
966 };
967 lines.push(format!(
968 " {} {} {}",
969 self.colored(ic.upload, Color::DarkYellow),
970 pi.name.name,
971 self.colored(&size, Color::DarkGrey),
972 ));
973 }
974 }
975
976 lines
977 }
978
979 fn render_dashboard_view(&self, state: &State) -> Vec<String> {
980 let now = current_time();
981 let duration = now - state.start_time;
982 let running = state.full_summary.running_builds.len();
983 let completed = state.full_summary.completed_builds.len();
984 let planned = state.full_summary.planned_builds.len();
985 let failed = state.full_summary.failed_builds.len();
986 let dl = state.full_summary.running_downloads.len();
987 let ul = state.full_summary.running_uploads.len();
988
989 if running + completed + planned + failed + dl + ul == 0 {
990 return vec![];
991 }
992
993 let ic = self.ic();
994 let sep = self.colored(&"─".repeat(44), Color::DarkBlue);
995 let pipe = self.colored("│", Color::DarkBlue);
996
997 let title = state
998 .forest_roots
999 .first()
1000 .and_then(|&id| state.get_derivation_info(id))
1001 .map_or_else(|| "Build".to_string(), |info| info.name.name.clone());
1002
1003 let host = state
1004 .full_summary
1005 .running_builds
1006 .values()
1007 .find_map(|b| {
1008 match &b.host {
1009 cognos::Host::Remote(h) => Some(h.clone()),
1010 _ => None,
1011 }
1012 })
1013 .unwrap_or_else(|| "localhost".to_string());
1014
1015 let (status_icon, status_color, status_label) = if running > 0 {
1016 (ic.running, Color::DarkYellow, "building")
1017 } else if planned > 0 || dl > 0 {
1018 (ic.planned, Color::DarkBlue, "waiting")
1019 } else if failed > 0 {
1020 (ic.failed, Color::DarkRed, "failed")
1021 } else {
1022 (ic.done, Color::DarkGreen, "done")
1023 };
1024 let status_str =
1025 format!("{} {status_label}", self.colored(status_icon, status_color));
1026
1027 let duration_str = self.format_duration(duration);
1028 let host_s = self.colored(&host, Color::DarkMagenta);
1029 let dur_s = self.colored(&duration_str, Color::DarkGrey);
1030 let fail_s = if failed > 0 && self.config.use_color {
1031 format!(
1032 "{}\x1b[1m{failed}\x1b[0m{}",
1033 SetForegroundColor(Color::DarkRed),
1034 ResetColor
1035 )
1036 } else {
1037 failed.to_string()
1038 };
1039 let summary_str = format!(
1040 "jobs={} ok={} failed={fail_s} total={dur_s}",
1041 self.num_str(running + completed + planned + failed),
1042 self.num_str(completed),
1043 );
1044
1045 let header = format!(
1046 "{} BUILD GRAPH: {title}",
1047 self.colored("┏━", Color::DarkBlue)
1048 );
1049
1050 vec![
1051 header,
1052 sep.clone(),
1053 format!("{:<12} {pipe} {host_s}", "Host"),
1054 format!("{:<12} {pipe} {status_str}", "Status"),
1055 format!("{:<12} {pipe} {dur_s}", "Duration"),
1056 sep,
1057 format!("{:<12} {pipe} {summary_str}", "Summary"),
1058 ]
1059 }
1060
1061 fn render_dashboard_final(&self, state: &State) -> Vec<String> {
1062 let duration = current_time() - state.start_time;
1063 let completed = state.full_summary.completed_builds.len();
1064 let failed = state.full_summary.failed_builds.len();
1065 let now = chrono::Local::now();
1066 let at = now.format("%H:%M:%S");
1067
1068 let ic = self.ic();
1069 let sep = self.colored(&"─".repeat(44), Color::DarkBlue);
1070 let pipe = self.colored("│", Color::DarkBlue);
1071
1072 let title = state
1073 .forest_roots
1074 .first()
1075 .and_then(|&id| state.get_derivation_info(id))
1076 .map_or_else(|| "Build".to_string(), |info| info.name.name.clone());
1077
1078 let host = state
1079 .full_summary
1080 .completed_builds
1081 .values()
1082 .find_map(|b| {
1083 match &b.host {
1084 cognos::Host::Remote(h) => Some(h.clone()),
1085 _ => None,
1086 }
1087 })
1088 .or_else(|| {
1089 state.full_summary.failed_builds.values().find_map(|b| {
1090 match &b.host {
1091 cognos::Host::Remote(h) => Some(h.clone()),
1092 _ => None,
1093 }
1094 })
1095 })
1096 .unwrap_or_else(|| "localhost".to_string());
1097
1098 let (status_icon, status_color, status_label) =
1099 if failed > 0 || !state.nix_errors.is_empty() {
1100 (ic.failed, Color::DarkRed, format!("failed at {at}"))
1101 } else {
1102 (ic.done, Color::DarkGreen, format!("finished at {at}"))
1103 };
1104 let status_str =
1105 format!("{} {status_label}", self.colored(status_icon, status_color));
1106
1107 let duration_str = self.format_duration(duration);
1108 let host_s = self.colored(&host, Color::DarkMagenta);
1109 let dur_s = self.colored(&duration_str, Color::DarkGrey);
1110 let jobs = completed + failed;
1111 let fail_s = if failed > 0 && self.config.use_color {
1112 format!(
1113 "{}\x1b[1m{failed}\x1b[0m{}",
1114 SetForegroundColor(Color::DarkRed),
1115 ResetColor
1116 )
1117 } else {
1118 failed.to_string()
1119 };
1120 let summary_str = format!(
1121 "jobs={} ok={} failed={fail_s} total={dur_s}",
1122 self.num_str(jobs),
1123 self.num_str(completed),
1124 );
1125
1126 let header = format!(
1127 "{} BUILD GRAPH: {title}",
1128 self.colored("┏━", Color::DarkBlue)
1129 );
1130
1131 vec![
1132 header,
1133 sep.clone(),
1134 format!("{:<12} {pipe} {host_s}", "Host"),
1135 format!("{:<12} {pipe} {status_str}", "Status"),
1136 format!("{:<12} {pipe} {dur_s}", "Duration"),
1137 sep,
1138 format!("{:<12} {pipe} {summary_str}", "Summary"),
1139 ]
1140 }
1141
1142 fn render_tree_view(&self, state: &State) -> Vec<String> {
1143 let visible_roots: Vec<DerivationId> = state
1154 .forest_roots
1155 .iter()
1156 .copied()
1157 .filter(|&drv_id| {
1158 state
1159 .get_derivation_info(drv_id)
1160 .map(|info| self.node_is_visible(info))
1161 .unwrap_or(false)
1162 })
1163 .collect();
1164
1165 if visible_roots.is_empty() {
1166 return Vec::new();
1167 }
1168
1169 let forest = self.build_forest(state, &visible_roots);
1170
1171 if forest.is_empty() {
1172 return Vec::new();
1173 }
1174
1175 let mut lines = Vec::new();
1176 lines.push(format!(
1177 "{} Dependency Graph:",
1178 self.colored("┏━", Color::DarkBlue)
1179 ));
1180
1181 let n = forest.len();
1182 if n == 1 {
1183 self.render_tree_node(state, &forest[0], &mut lines);
1186 } else {
1187 for (rev_i, node) in forest.iter().rev().enumerate() {
1189 let is_top_tree = rev_i == 0;
1191
1192 let mut tree_lines: Vec<String> = Vec::new();
1193 self.render_tree_node(state, node, &mut tree_lines);
1194
1195 for (line_idx, tree_line) in tree_lines.iter().enumerate() {
1197 let connector = if is_top_tree {
1199 if line_idx == 0 {
1200 self.colored("┌─ ", Color::DarkBlue)
1201 } else {
1202 " ".to_string()
1203 }
1204 } else if line_idx == 0 {
1205 self.colored("├─ ", Color::DarkBlue)
1206 } else {
1207 self.colored("│ ", Color::DarkBlue)
1208 };
1209 lines.push(format!("{connector}{tree_line}"));
1210 }
1211 }
1212 }
1213
1214 lines
1215 }
1216
1217 fn node_is_visible(&self, info: &crate::state::DerivationInfo) -> bool {
1222 use crate::state::DependencySummary;
1223 let summary_non_empty = |s: &DependencySummary| {
1224 !s.planned_builds.is_empty()
1225 || !s.running_builds.is_empty()
1226 || !s.completed_builds.is_empty()
1227 || !s.failed_builds.is_empty()
1228 || !s.running_downloads.is_empty()
1229 || !s.running_uploads.is_empty()
1230 || !s.completed_downloads.is_empty()
1231 || !s.completed_uploads.is_empty()
1232 };
1233
1234 match &info.build_status {
1235 BuildStatus::Unknown => summary_non_empty(&info.dependency_summary),
1236 _ => true,
1237 }
1238 }
1239
1240 fn build_forest(
1241 &self,
1242 state: &State,
1243 roots: &[DerivationId],
1244 ) -> Vec<TreeNode> {
1245 let mut forest = Vec::new();
1246 let mut visited = HashSet::new();
1247
1248 for &root_id in roots {
1249 if let Some(node) = self.build_tree_node(state, root_id, &mut visited, 0)
1250 {
1251 forest.push(node);
1252 }
1253 }
1254
1255 forest
1256 }
1257
1258 fn build_tree_node(
1259 &self,
1260 state: &State,
1261 drv_id: DerivationId,
1262 visited: &mut HashSet<DerivationId>,
1263 depth: usize,
1264 ) -> Option<TreeNode> {
1265 if visited.contains(&drv_id) {
1266 return None;
1267 }
1268 visited.insert(drv_id);
1269
1270 if depth >= self.config.max_tree_depth {
1271 return Some(TreeNode {
1272 drv_id,
1273 children: Vec::new(),
1274 });
1275 }
1276
1277 let drv_info = state.get_derivation_info(drv_id)?;
1278
1279 let mut children: Vec<TreeNode> = Vec::new();
1280 for input in &drv_info.input_derivations {
1281 let child_info = match state.get_derivation_info(input.derivation) {
1282 Some(i) => i,
1283 None => continue,
1284 };
1285
1286 if !self.node_is_visible(child_info) {
1288 continue;
1289 }
1290
1291 if let Some(child) =
1292 self.build_tree_node(state, input.derivation, visited, depth + 1)
1293 {
1294 children.push(child);
1295 }
1296 }
1297
1298 children.sort_by_key(|c| {
1300 state
1301 .get_derivation_info(c.drv_id)
1302 .map(|i| self.tree_sort_priority(&i.build_status))
1303 .unwrap_or(u8::MAX)
1304 });
1305
1306 Some(TreeNode { drv_id, children })
1307 }
1308
1309 fn tree_sort_priority(&self, status: &BuildStatus) -> u8 {
1312 match status {
1313 BuildStatus::Failed { .. } => 0,
1314 BuildStatus::Building(_) => 1,
1315 BuildStatus::Planned => 2,
1316 BuildStatus::Unknown => 3,
1317 BuildStatus::Built { .. } => 4,
1318 }
1319 }
1320
1321 fn render_tree_node(
1332 &self,
1333 state: &State,
1334 node: &TreeNode,
1335 lines: &mut Vec<String>,
1336 ) {
1337 let info = match state.get_derivation_info(node.drv_id) {
1338 Some(info) => info,
1339 None => return,
1340 };
1341
1342 let n = node.children.len();
1345 for (rev_i, child) in node.children.iter().rev().enumerate() {
1346 let is_top = rev_i == 0;
1347 self.render_tree_child(
1348 state,
1349 child,
1350 lines,
1351 is_top,
1352 &self.colored("┃ ", Color::DarkBlue),
1353 );
1354 }
1355
1356 let _ = n;
1357 let mut line = String::new();
1358 line.push_str(&self.colored("┃ ", Color::DarkBlue));
1359 line.push_str(&self.format_node_content(state, info, false));
1360 lines.push(line);
1361 }
1362
1363 fn render_tree_child(
1370 &self,
1371 state: &State,
1372 node: &TreeNode,
1373 lines: &mut Vec<String>,
1374 is_top: bool,
1375 prefix: &str,
1376 ) {
1377 let info = match state.get_derivation_info(node.drv_id) {
1378 Some(info) => info,
1379 None => return,
1380 };
1381
1382 let child_prefix = if is_top {
1386 format!("{prefix} ")
1387 } else {
1388 format!("{prefix}{}", self.colored("│ ", Color::DarkBlue))
1389 };
1390
1391 for (rev_i, child) in node.children.iter().rev().enumerate() {
1392 let grandchild_is_top = rev_i == 0;
1393 self.render_tree_child(
1394 state,
1395 child,
1396 lines,
1397 grandchild_is_top,
1398 &child_prefix,
1399 );
1400 }
1401
1402 let mut line = String::new();
1404 line.push_str(prefix);
1405
1406 let connector = if is_top { "┌─ " } else { "├─ " };
1408 line.push_str(&self.colored(connector, Color::DarkBlue));
1409
1410 let is_leaf = node.children.is_empty();
1411 line.push_str(&self.format_node_content(state, info, is_leaf));
1412
1413 lines.push(line);
1414 }
1415
1416 fn format_node_content(
1420 &self,
1421 state: &State,
1422 info: &crate::state::DerivationInfo,
1423 is_leaf: bool,
1424 ) -> String {
1425 let ic = self.ic();
1426 let mut s = String::new();
1427
1428 if let Some((icon, color)) = self.get_status_icon(&info.build_status) {
1430 s.push_str(&self.colored(icon, color));
1431 s.push(' ');
1432 }
1433 let raw_name = self.truncate_name(&info.name.name, 50);
1435 let name_str = match &info.build_status {
1436 BuildStatus::Building(_) => {
1437 self.colored_bold(&raw_name, Color::DarkYellow)
1438 },
1439 BuildStatus::Failed { .. } => {
1440 self.colored_bold(&raw_name, Color::DarkRed)
1441 },
1442 BuildStatus::Built { .. } => self.colored(&raw_name, Color::DarkGreen),
1443 _ => raw_name,
1444 };
1445 s.push_str(&name_str);
1446
1447 match &info.build_status {
1448 BuildStatus::Building(build_info) => {
1449 if let cognos::Host::Remote(ref host_name) = build_info.host {
1451 s.push_str(
1452 &self.colored(&format!(" on {host_name}"), Color::Magenta),
1453 );
1454 }
1455
1456 if let Some(activity_id) = build_info.activity_id
1458 && let Some(activity) = state.activities.get(&activity_id)
1459 && let Some(phase) = &activity.phase
1460 {
1461 s.push_str(
1462 &self.colored_bold(&format!(" ({phase})"), Color::DarkGrey),
1463 );
1464 }
1465
1466 let elapsed = current_time() - build_info.start;
1467
1468 if self.config.show_timers && elapsed > 1.0 {
1470 s.push_str(&self.colored(
1471 &format!(" {} {}", ic.clock, self.format_duration(elapsed)),
1472 Color::DarkGrey,
1473 ));
1474 if let Some(estimate_secs) = build_info.estimate {
1476 s.push_str(&self.colored(
1477 &format!(
1478 " ({} {})",
1479 ic.estimate,
1480 self.format_duration(estimate_secs as f64)
1481 ),
1482 Color::DarkGrey,
1483 ));
1484 }
1485 }
1486 },
1487 BuildStatus::Failed {
1488 info: build_info,
1489 fail,
1490 } => {
1491 if let cognos::Host::Remote(ref host_name) = build_info.host {
1493 s.push_str(&format!(" on {host_name}"));
1494 }
1495
1496 let fail_str = match &fail.fail_type {
1498 crate::state::FailType::BuildFailed(code) => {
1499 format!(" failed with exit code {code}")
1500 },
1501 crate::state::FailType::Timeout => " timed out".to_string(),
1502 crate::state::FailType::HashMismatch => " hash mismatch".to_string(),
1503 crate::state::FailType::DependencyFailed => {
1504 " dependency failed".to_string()
1505 },
1506 crate::state::FailType::Unknown => " failed".to_string(),
1507 };
1508 s.push_str(&self.colored(&fail_str, Color::DarkRed));
1509
1510 if let Some(activity_id) = build_info.activity_id
1512 && let Some(activity) = state.activities.get(&activity_id)
1513 && let Some(phase) = &activity.phase
1514 {
1515 s.push_str(&self.colored(&format!(" in {phase}"), Color::DarkGrey));
1516 }
1517
1518 if self.config.show_timers {
1520 let duration = fail.at - build_info.start;
1521 if duration > 1.0 {
1522 s.push_str(&self.colored(
1523 &format!(" {} {}", ic.clock, self.format_duration(duration)),
1524 Color::DarkGrey,
1525 ));
1526 }
1527 }
1528 },
1529 BuildStatus::Built {
1530 info: build_info,
1531 end,
1532 } => {
1533 if let cognos::Host::Remote(ref host_name) = build_info.host {
1535 s.push_str(
1536 &self.colored(&format!(" on {host_name}"), Color::DarkGrey),
1537 );
1538 }
1539 if self.config.show_timers {
1541 let duration = end - build_info.start;
1542 if duration > 1.0 {
1543 s.push_str(&self.colored(
1544 &format!(" {} {}", ic.clock, self.format_duration(duration)),
1545 Color::DarkGrey,
1546 ));
1547 }
1548 }
1549 },
1550 BuildStatus::Planned => {
1551 if is_leaf {
1554 let waiting = self.format_waiting_summary(&info.dependency_summary);
1555 if !waiting.is_empty() {
1556 s.push_str(
1557 &self
1558 .colored(&format!(" waiting for {waiting}"), Color::DarkGrey),
1559 );
1560 }
1561 }
1562 },
1563 BuildStatus::Unknown => {},
1564 }
1565
1566 s
1567 }
1568
1569 fn format_waiting_summary(
1572 &self,
1573 summary: &crate::state::DependencySummary,
1574 ) -> String {
1575 let ic = self.ic();
1576 let mut parts: Vec<String> = Vec::new();
1577
1578 let failed = summary.failed_builds.len();
1579 if failed > 0 {
1580 parts.push(
1581 self.colored(&format!("{} {}", ic.failed, failed), Color::DarkRed),
1582 );
1583 }
1584
1585 let running = summary.running_builds.len();
1586 if running > 0 {
1587 parts.push(
1588 self.colored(&format!("{} {}", ic.running, running), Color::DarkYellow),
1589 );
1590 }
1591
1592 let planned = summary.planned_builds.len();
1593 if planned > 0 {
1594 parts.push(
1595 self.colored(&format!("{} {}", ic.planned, planned), Color::DarkBlue),
1596 );
1597 }
1598
1599 parts.join(" ")
1600 }
1601
1602 fn get_status_icon(
1603 &self,
1604 status: &BuildStatus,
1605 ) -> Option<(&'static str, Color)> {
1606 let ic = self.ic();
1607 match status {
1608 BuildStatus::Building(_) => Some((ic.running, Color::DarkYellow)),
1609 BuildStatus::Planned => Some((ic.planned, Color::DarkBlue)),
1610 BuildStatus::Built { .. } => Some((ic.done, Color::DarkGreen)),
1611 BuildStatus::Failed { .. } => Some((ic.failed, Color::DarkRed)),
1612 BuildStatus::Unknown => None,
1614 }
1615 }
1616
1617 fn ic(&self) -> &'static Icons {
1619 self.config.icons
1620 }
1621
1622 fn colored(&self, text: &str, color: Color) -> String {
1623 if self.config.use_color {
1624 format!("{}{}{}", SetForegroundColor(color), text, ResetColor)
1625 } else {
1626 text.to_string()
1627 }
1628 }
1629
1630 fn colored_bold(&self, text: &str, color: Color) -> String {
1632 if self.config.use_color {
1633 format!(
1634 "{}\x1b[1m{}\x1b[0m{}",
1635 SetForegroundColor(color),
1636 text,
1637 ResetColor
1638 )
1639 } else {
1640 text.to_string()
1641 }
1642 }
1643
1644 fn count_colored(&self, icon: &str, n: usize, active_color: Color) -> String {
1646 let icon_s = self.colored(icon, active_color);
1647 let num_s = if n > 0 && self.config.use_color {
1648 format!("\x1b[1m{n}\x1b[0m")
1649 } else {
1650 n.to_string()
1651 };
1652 format!("{icon_s} {num_s}")
1653 }
1654
1655 fn num_str(&self, n: usize) -> String {
1658 if n > 0 && self.config.use_color {
1659 format!("\x1b[1m{n}\x1b[0m")
1660 } else {
1661 n.to_string()
1662 }
1663 }
1664
1665 pub fn format_duration(&self, secs: f64) -> String {
1666 if secs < 60.0 {
1667 format!("{secs:.0}s")
1668 } else if secs < 3600.0 {
1669 format!("{:.0}m{:.0}s", secs / 60.0, secs % 60.0)
1670 } else {
1671 format!("{:.0}h{:.0}m", secs / 3600.0, (secs % 3600.0) / 60.0)
1672 }
1673 }
1674
1675 fn truncate_name(&self, name: &str, max_len: usize) -> String {
1676 if name.len() <= max_len {
1677 name.to_string()
1678 } else {
1679 format!("{}…", &name[..max_len.saturating_sub(1)])
1680 }
1681 }
1682
1683 fn format_bytes(&self, transferred: u64, total: u64) -> String {
1684 let pct = if total > 0 {
1685 (transferred as f64 / total as f64 * 100.0) as u64
1686 } else {
1687 0
1688 };
1689 format_size(total) + &self.colored(&format!(" ({pct}%)"), Color::DarkGrey)
1690 }
1691}
1692
1693fn format_size(bytes: u64) -> String {
1694 if bytes < 1024 {
1695 format!("{bytes} B")
1696 } else if bytes < 1024 * 1024 {
1697 format!("{:.1} KiB", bytes as f64 / 1024.0)
1698 } else if bytes < 1024 * 1024 * 1024 {
1699 format!("{:.1} MiB", bytes as f64 / (1024.0 * 1024.0))
1700 } else {
1701 format!("{:.1} GiB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
1702 }
1703}