OXYGENHUB
# 7bc302317004 oxygen / src / cli / ui.rs
3.1 KiB · Rust · last changed Pack merging above 16 packs; modern CLI presentation (colour, spinners, --plain, NO_COLOR); oxy prompt with manifest-based dirty check and docs/prompt.md 1 month ago source blame history raw
1
//! Terminal presentation: colour, emphasis, and a live spinner while a
2
//! command works. Everything degrades to plain text when stdout is not a
3
//! terminal, when `NO_COLOR` is set, or with `--plain`.
4
5
use console::{style, StyledObject, Term};
6
use indicatif::{ProgressBar, ProgressStyle};
7
use std::sync::atomic::{AtomicBool, Ordering};
8
use std::time::Duration;
9
10
static PLAIN: AtomicBool = AtomicBool::new(false);
11
static INIT: std::sync::Once = std::sync::Once::new();
12
13
/// Decide once whether to use colour and animation.
14
pub fn init(plain_flag: bool) {
15
    INIT.call_once(|| {
16
        let no_color = std::env::var_os("NO_COLOR").is_some();
17
        let tty = Term::stdout().features().is_attended();
18
        let plain = plain_flag || no_color || !tty;
19
        PLAIN.store(plain, Ordering::Relaxed);
20
        console::set_colors_enabled(!plain);
21
    });
22
}
23
24
pub fn plain() -> bool {
25
    PLAIN.load(Ordering::Relaxed)
26
}
27
28
fn s<T: std::fmt::Display>(v: T) -> StyledObject<T> {
29
    style(v)
30
}
31
32
/// A ChangeId prefix.
33
pub fn id<T: std::fmt::Display>(v: T) -> String {
34
    s(v).cyan().bold().to_string()
35
}
36
/// A hash prefix.
37
pub fn hash<T: std::fmt::Display>(v: T) -> String {
38
    s(v).dim().to_string()
39
}
40
pub fn path<T: std::fmt::Display>(v: T) -> String {
41
    s(v).bold().to_string()
42
}
43
pub fn ok<T: std::fmt::Display>(v: T) -> String {
44
    s(v).green().to_string()
45
}
46
pub fn warn<T: std::fmt::Display>(v: T) -> String {
47
    s(v).yellow().to_string()
48
}
49
pub fn err<T: std::fmt::Display>(v: T) -> String {
50
    s(v).red().bold().to_string()
51
}
52
pub fn dim<T: std::fmt::Display>(v: T) -> String {
53
    s(v).dim().to_string()
54
}
55
pub fn heading<T: std::fmt::Display>(v: T) -> String {
56
    s(v).bold().underlined().to_string()
57
}
58
/// A status tag: green for additions, yellow for modifications, red for removals and conflicts.
59
pub fn tag(t: &str) -> String {
60
    match t {
61
        "A" => s(t).green().bold().to_string(),
62
        "M" | "X" => s(t).yellow().bold().to_string(),
63
        "D" | "C!" => s(t).red().bold().to_string(),
64
        "R" => s(t).cyan().bold().to_string(),
65
        other => other.to_string(),
66
    }
67
}
68
69
/// A live spinner with a message, shown only on a terminal. Drop or call
70
/// `done` to clear it; `done` prints the final line in its place.
71
pub struct Spinner {
72
    bar: Option<ProgressBar>,
73
}
74
75
pub fn spinner(msg: &str) -> Spinner {
76
    if plain() {
77
        return Spinner { bar: None };
78
    }
79
    let bar = ProgressBar::new_spinner();
80
    bar.set_style(ProgressStyle::with_template("{spinner:.cyan} {msg}").unwrap().tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", "✔"]));
81
    bar.set_message(msg.to_string());
82
    bar.enable_steady_tick(Duration::from_millis(80));
83
    Spinner { bar: Some(bar) }
84
}
85
86
impl Spinner {
87
    /// Replace the spinner with a final line (or print it plainly).
88
    pub fn done(self, msg: &str) {
89
        match &self.bar {
90
            Some(b) => b.finish_with_message(msg.to_string()),
91
            None => println!("{msg}"),
92
        }
93
    }
94
    /// Clear the spinner without printing.
95
    pub fn clear(self) {
96
        if let Some(b) = &self.bar {
97
            b.finish_and_clear();
98
        }
99
    }
100
}