Skip to main content

tirith_core/
style.rs

1use crate::verdict::Severity;
2use owo_colors::OwoColorize;
3
4/// Which output stream color decisions should be based on.
5/// Commands route human output to different streams (e.g., check uses stderr,
6/// warnings uses stdout), so color must be evaluated per-stream.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum Stream {
9    Stdout,
10    Stderr,
11}
12
13/// Check if color should be used for a given output stream.
14/// Respects the `NO_COLOR` env var (https://no-color.org/) and TTY detection.
15///
16/// Per the NO_COLOR spec, the presence of the variable is sufficient to disable
17/// color, regardless of its value (including empty string).
18pub fn use_color_for(stream: Stream) -> bool {
19    if std::env::var_os("NO_COLOR").is_some() {
20        return false;
21    }
22    match stream {
23        Stream::Stderr => is_terminal::is_terminal(std::io::stderr()),
24        Stream::Stdout => is_terminal::is_terminal(std::io::stdout()),
25    }
26}
27
28/// Format a severity label with color appropriate for the given stream.
29/// Returns a bracketed severity label like `[CRITICAL]` with ANSI color when
30/// color is supported, or plain text otherwise.
31pub fn severity_label(severity: &Severity, stream: Stream) -> String {
32    let label = format!("[{}]", severity);
33    if !use_color_for(stream) {
34        return label;
35    }
36    match severity {
37        Severity::Critical => label.bright_red().to_string(),
38        Severity::High => label.red().to_string(),
39        Severity::Medium => label.yellow().to_string(),
40        Severity::Low => label.cyan().to_string(),
41        Severity::Info => label.dimmed().to_string(),
42    }
43}
44
45/// Format text as bold if color is enabled for the stream.
46pub fn bold(text: &str, stream: Stream) -> String {
47    if use_color_for(stream) {
48        text.bold().to_string()
49    } else {
50        text.to_string()
51    }
52}
53
54/// Format text as bold + red if color is enabled. Falls back to plain text.
55pub fn bold_red(text: &str, stream: Stream) -> String {
56    if use_color_for(stream) {
57        text.bold().red().to_string()
58    } else {
59        text.to_string()
60    }
61}
62
63/// Format text as green if color is enabled.
64pub fn green(text: &str, stream: Stream) -> String {
65    if use_color_for(stream) {
66        text.green().to_string()
67    } else {
68        text.to_string()
69    }
70}
71
72/// Format text as red if color is enabled.
73pub fn red(text: &str, stream: Stream) -> String {
74    if use_color_for(stream) {
75        text.red().to_string()
76    } else {
77        text.to_string()
78    }
79}
80
81/// Format text as yellow if color is enabled.
82pub fn yellow(text: &str, stream: Stream) -> String {
83    if use_color_for(stream) {
84        text.yellow().to_string()
85    } else {
86        text.to_string()
87    }
88}
89
90/// Format text as dimmed if color is enabled for the stream.
91pub fn dim(text: &str, stream: Stream) -> String {
92    if use_color_for(stream) {
93        text.dimmed().to_string()
94    } else {
95        text.to_string()
96    }
97}
98
99/// Format a pass/success marker (green checkmark or `[ok]`).
100pub fn pass_mark(stream: Stream) -> String {
101    if use_color_for(stream) {
102        "\u{2713}".green().to_string()
103    } else {
104        "[ok]".to_string()
105    }
106}
107
108/// Format a fail marker (red X or `[!!]`).
109pub fn fail_mark(stream: Stream) -> String {
110    if use_color_for(stream) {
111        "\u{2717}".red().to_string()
112    } else {
113        "[!!]".to_string()
114    }
115}