Skip to main content

podbox/
ui.rs

1//! Minimal terminal-output helpers for the podbox CLI.
2//!
3//! Contract (REVIEW.md "Output and error contract"):
4//! - Read commands print data on stdout; diagnostics always go to stderr.
5//! - [`step`]/[`ok`] are human progress lines: suppressed by `--quiet`.
6//! - [`warn`], [`hint`], and [`error`] are always shown.
7//! - Colors use `owo-colors` + `supports-color`, which honor `NO_COLOR`.
8//!
9//! This module is intentionally not a framework: plain functions, no macros,
10//! no global logger. Verbosity beyond progress lines goes through `tracing`.
11
12use std::io::Write;
13use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
14
15use owo_colors::{OwoColorize, Stream};
16
17static QUIET: AtomicBool = AtomicBool::new(false);
18static VERBOSE: AtomicU8 = AtomicU8::new(0);
19
20/// Enable/disable quiet mode (wired to the global `--quiet` flag).
21pub fn set_quiet(quiet: bool) {
22    QUIET.store(quiet, Ordering::Relaxed);
23}
24
25pub fn is_quiet() -> bool {
26    QUIET.load(Ordering::Relaxed)
27}
28
29/// Store the `-v` count (0 = info, 1 = debug, 2+ = trace).
30pub fn set_verbose(count: u8) {
31    VERBOSE.store(count, Ordering::Relaxed);
32}
33
34/// True when any `--verbose` level is active. Long-running child processes
35/// (podman build/pull) stream their output to the terminal in this mode
36/// instead of being captured to the build log.
37pub fn is_verbose() -> bool {
38    VERBOSE.load(Ordering::Relaxed) > 0
39}
40
41fn write_stderr(line: &str) {
42    let _ = writeln!(std::io::stderr(), "{line}");
43}
44
45/// Print a progress line ("→ msg") to stderr unless quiet.
46pub fn step(msg: &str) {
47    if is_quiet() {
48        return;
49    }
50    let glyph = "→".if_supports_color(Stream::Stderr, |t| t.dimmed());
51    write_stderr(&format!("{glyph} {msg}"));
52}
53
54/// Print a success line ("✔ msg") to stderr unless quiet.
55pub fn ok(msg: &str) {
56    if is_quiet() {
57        return;
58    }
59    let mark = "✔".if_supports_color(Stream::Stderr, |t| t.green());
60    let body = msg.if_supports_color(Stream::Stderr, |t| t.green());
61    write_stderr(&format!("{mark} {body}"));
62}
63
64/// Print a warning to stderr (never suppressed).
65pub fn warn(msg: &str) {
66    let mark = "!".if_supports_color(Stream::Stderr, |t| t.yellow());
67    let body = msg.if_supports_color(Stream::Stderr, |t| t.yellow());
68    write_stderr(&format!("{mark} {body}"));
69}
70
71/// Print an actionable hint block to stderr (never suppressed).
72// No caller yet: wired up when BuildFailed grows its log-path hint (PR 3)
73// and by doctor/recover (PR 5). Part of the module's documented contract.
74#[allow(dead_code)]
75pub fn hint(lines: &[&str]) {
76    if lines.is_empty() {
77        return;
78    }
79    let head = "Hint:".if_supports_color(Stream::Stderr, |t| t.cyan().bold().to_string());
80    let mut err = std::io::stderr();
81    let _ = writeln!(err, "\n{head} {}", lines[0]);
82    for l in &lines[1..] {
83        let _ = writeln!(err, "      {l}");
84    }
85}
86
87/// Print the top-level error line to stderr (never suppressed).
88///
89/// Used once from `main`; command code should return errors instead of
90/// printing them so exit-code mapping stays centralized.
91pub fn error(msg: &str) {
92    let head = "Error:".if_supports_color(Stream::Stderr, |t| t.red().bold().to_string());
93    write_stderr(&format!("\n{head} {msg}"));
94}