umshctl/
output.rs

1//! Shared presentation: the colour decision, the field-tinted styling
2//! the capture decoder needs, and the key/value report layout every
3//! command prints.
4
5use std::io::IsTerminal;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8/// When decoded output carries ANSI colour.
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
10pub enum ColorChoice {
11    #[default]
12    Auto,
13    #[value(alias = "yes")]
14    Always,
15    #[value(alias = "no")]
16    Never,
17}
18
19impl ColorChoice {
20    /// Resolve to a concrete answer for this run. `auto` honours a
21    /// non-terminal stdout (a pipe or a redirect to a log), `NO_COLOR`,
22    /// and `TERM=dumb`.
23    pub fn enabled(self) -> bool {
24        match self {
25            Self::Always => true,
26            Self::Never => false,
27            Self::Auto => {
28                std::io::stdout().is_terminal()
29                    && std::env::var_os("NO_COLOR").is_none()
30                    && std::env::var("TERM").is_ok_and(|term| term != "dumb")
31            }
32        }
33    }
34}
35
36/// Resolved once at startup so every module can ask without threading a
37/// flag through call after call. A tool this shape has exactly one
38/// output stream and one answer for it.
39static COLOR: AtomicBool = AtomicBool::new(false);
40
41pub fn set_color(enabled: bool) {
42    COLOR.store(enabled, Ordering::Relaxed);
43}
44
45pub fn color() -> bool {
46    COLOR.load(Ordering::Relaxed)
47}
48
49/// Wrap `text` in the given SGR parameters, or return it unchanged when
50/// the output is not colorized.
51pub fn styled(text: &str, sgr: &str, color: bool) -> String {
52    if color {
53        format!("\x1b[{sgr}m{text}\x1b[0m")
54    } else {
55        text.to_owned()
56    }
57}
58
59/// Column at which a report's values line up. Wide enough for the
60/// longest label a report prints (`capabilities:`).
61const LABEL_WIDTH: usize = 14;
62
63/// One line of a key/value report: `label:` in the left column, value in
64/// the right.
65pub fn field(label: &str, value: impl std::fmt::Display) {
66    println!("{:<LABEL_WIDTH$}{value}", format!("{label}:"));
67}
68
69/// A nested line of a key/value report, indented under its parent.
70pub fn subfield(label: &str, value: impl std::fmt::Display) {
71    println!("  {:<width$}{value}", format!("{label}:"), width = 14);
72}
73
74/// A note about what the tool just did, or declined to do. Stays on
75/// stdout with the rest of the report; failures go through `Err`.
76pub fn note(text: impl std::fmt::Display) {
77    println!("note: {text}");
78}
79
80/// Something the user is likely to have gotten wrong, which did not stop
81/// the command from succeeding.
82pub fn warn(text: impl std::fmt::Display) {
83    eprintln!("warning: {text}");
84}
85
86/// Lowercase hex, no separators — the form every digest, id, and key in
87/// this tool's output uses.
88pub fn hex(bytes: &[u8]) -> String {
89    use std::fmt::Write as _;
90    let mut text = String::with_capacity(bytes.len() * 2);
91    for byte in bytes {
92        let _ = write!(text, "{byte:02x}");
93    }
94    text
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn color_choice_resolves_the_unconditional_forms() {
103        assert!(ColorChoice::Always.enabled());
104        assert!(!ColorChoice::Never.enabled());
105    }
106
107    #[test]
108    fn styling_is_inert_without_color() {
109        assert_eq!(styled("x", "1;97", false), "x");
110        assert_eq!(styled("x", "1;97", true), "\x1b[1;97mx\x1b[0m");
111    }
112
113    #[test]
114    fn hex_has_no_separators() {
115        assert_eq!(hex(&[0x0a, 0xff]), "0aff");
116        assert_eq!(hex(&[]), "");
117    }
118}