inspect_format/color_choice.rs
1//! Terminal color choice and detection logic.
2
3use std::io::IsTerminal;
4
5/// User preference for emitting ANSI color escape codes.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum ColorChoice {
8 /// Automatically enable color if writing to a TTY and environment allows it.
9 #[default]
10 Auto,
11
12 /// Always emit ANSI color codes, even when redirected or piped.
13 Always,
14
15 /// Never emit ANSI color codes.
16 Never,
17}
18
19impl ColorChoice {
20 /// Determine whether color should be active given the terminal/TTY status of the output.
21 ///
22 /// Respects:
23 /// - `NO_COLOR` (<https://no-color.org>)
24 /// - `FORCE_COLOR` (<https://force-color.org>)
25 /// - `TERM=dumb`
26 /// - Stream TTY status
27 pub fn should_color(self, is_terminal: bool) -> bool {
28 match self {
29 Self::Always => true,
30 Self::Never => false,
31 Self::Auto => Self::detect_auto(is_terminal),
32 }
33 }
34
35 /// Automatically detect whether color is supported for the current environment.
36 pub fn detect_auto(is_terminal: bool) -> bool {
37 Self::detect_with(is_terminal, |k| std::env::var(k).ok())
38 }
39
40 /// Internal helper that tests environment variables using a mockable lookup closure.
41 pub fn detect_with(is_terminal: bool, get_var: impl Fn(&str) -> Option<String>) -> bool {
42 // 1. NO_COLOR takes precedence over automatic detection:
43 // Any non-empty string disables color.
44 if let Some(val) = get_var("NO_COLOR") {
45 if !val.is_empty() {
46 return false;
47 }
48 }
49
50 // 2. FORCE_COLOR forces color when set to non-empty and non-"0".
51 if let Some(val) = get_var("FORCE_COLOR") {
52 if !val.is_empty() && val != "0" {
53 return true;
54 }
55 }
56
57 // 3. TERM=dumb disables color.
58 if let Some(term) = get_var("TERM") {
59 if term == "dumb" {
60 return false;
61 }
62 }
63
64 // 4. Output stream must be an interactive terminal.
65 is_terminal
66 }
67
68 /// Resolve whether stdout should currently display colors.
69 pub fn resolve_stdout(self) -> bool {
70 self.should_color(std::io::stdout().is_terminal())
71 }
72
73 /// Resolve whether stderr should currently display colors.
74 pub fn resolve_stderr(self) -> bool {
75 self.should_color(std::io::stderr().is_terminal())
76 }
77}
78
79/// Color capability tier supported by the terminal.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
81pub enum ColorLevel {
82 /// Automatically detected from terminal capabilities and environment.
83 #[default]
84 Auto,
85
86 /// Full 24-bit Truecolor (16 million RGB colors).
87 Truecolor,
88
89 /// 8-bit ANSI 256-color palette.
90 Ansi256,
91
92 /// 4-bit standard ANSI 16-color palette.
93 Ansi16,
94
95 /// No colors supported or color disabled.
96 None,
97}
98
99impl ColorLevel {
100 /// Detect the terminal's color capability tier from environment variables.
101 pub fn detect() -> Self {
102 Self::detect_with(|k| std::env::var(k).ok())
103 }
104
105 /// Test color capability using a mockable environment lookup closure.
106 pub fn detect_with(get_var: impl Fn(&str) -> Option<String>) -> Self {
107 // Check if color is explicitly disabled
108 if let Some(val) = get_var("NO_COLOR") {
109 if !val.is_empty() {
110 return Self::None;
111 }
112 }
113
114 if let Some(term) = get_var("TERM") {
115 if term == "dumb" {
116 return Self::None;
117 }
118 }
119
120 // Truecolor check: COLORTERM=truecolor or 24bit
121 if let Some(colorterm) = get_var("COLORTERM") {
122 if colorterm == "truecolor" || colorterm == "24bit" {
123 return Self::Truecolor;
124 }
125 }
126
127 // Known truecolor / 256color terminal indicators in TERM
128 if let Some(term) = get_var("TERM") {
129 let term_lower = term.to_lowercase();
130 if term_lower.contains("direct")
131 || term_lower.contains("truecolor")
132 || term_lower.contains("kitty")
133 || term_lower.contains("alacritty")
134 || term_lower.contains("wezterm")
135 || term_lower.contains("ghostty")
136 {
137 return Self::Truecolor;
138 }
139 if term_lower.contains("256color") || term_lower.contains("xterm") {
140 return Self::Ansi256;
141 }
142 }
143
144 // Default baseline Unix terminal capability
145 Self::Ansi16
146 }
147}