cardanowall_cli/util/
color.rs1use std::io::IsTerminal;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum ColorChoice {
19 #[default]
21 Auto,
22 Always,
24 Never,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum Stream {
31 Stdout,
33 Stderr,
35}
36
37pub trait ColorEnv {
39 fn var(&self, key: &str) -> Option<String>;
41 fn is_terminal(&self, stream: Stream) -> bool;
43}
44
45pub struct SystemColorEnv;
47
48impl ColorEnv for SystemColorEnv {
49 fn var(&self, key: &str) -> Option<String> {
50 std::env::var(key).ok().filter(|v| !v.is_empty())
51 }
52 fn is_terminal(&self, stream: Stream) -> bool {
53 match stream {
54 Stream::Stdout => std::io::stdout().is_terminal(),
55 Stream::Stderr => std::io::stderr().is_terminal(),
56 }
57 }
58}
59
60#[must_use]
63pub fn should_color(
64 choice: ColorChoice,
65 json_mode: bool,
66 stream: Stream,
67 env: &dyn ColorEnv,
68) -> bool {
69 if json_mode {
71 return false;
72 }
73 match choice {
74 ColorChoice::Never => false,
75 ColorChoice::Always => true,
76 ColorChoice::Auto => {
77 if env.var("NO_COLOR").is_some() {
78 return false;
79 }
80 if env.var("CLICOLOR_FORCE").is_some() {
81 return true;
82 }
83 env.is_terminal(stream)
84 }
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use std::collections::HashMap;
92
93 #[derive(Default)]
94 struct FakeEnv {
95 vars: HashMap<String, String>,
96 stdout_tty: bool,
97 stderr_tty: bool,
98 }
99 impl ColorEnv for FakeEnv {
100 fn var(&self, key: &str) -> Option<String> {
101 self.vars.get(key).cloned().filter(|v| !v.is_empty())
102 }
103 fn is_terminal(&self, stream: Stream) -> bool {
104 match stream {
105 Stream::Stdout => self.stdout_tty,
106 Stream::Stderr => self.stderr_tty,
107 }
108 }
109 }
110
111 #[test]
112 fn json_mode_is_always_off() {
113 let env = FakeEnv {
114 stderr_tty: true,
115 ..FakeEnv::default()
116 };
117 assert!(!should_color(
118 ColorChoice::Always,
119 true,
120 Stream::Stderr,
121 &env
122 ));
123 }
124
125 #[test]
126 fn never_is_off_always_is_on() {
127 let env = FakeEnv::default();
128 assert!(!should_color(
129 ColorChoice::Never,
130 false,
131 Stream::Stderr,
132 &env
133 ));
134 assert!(should_color(
135 ColorChoice::Always,
136 false,
137 Stream::Stderr,
138 &env
139 ));
140 }
141
142 #[test]
143 fn no_color_env_forces_off_in_auto() {
144 let env = FakeEnv {
145 vars: HashMap::from([("NO_COLOR".to_string(), "1".to_string())]),
146 stderr_tty: true,
147 ..FakeEnv::default()
148 };
149 assert!(!should_color(
150 ColorChoice::Auto,
151 false,
152 Stream::Stderr,
153 &env
154 ));
155 }
156
157 #[test]
158 fn clicolor_force_turns_on_without_tty() {
159 let env = FakeEnv {
160 vars: HashMap::from([("CLICOLOR_FORCE".to_string(), "1".to_string())]),
161 ..FakeEnv::default()
162 };
163 assert!(should_color(ColorChoice::Auto, false, Stream::Stderr, &env));
164 }
165
166 #[test]
167 fn no_color_beats_clicolor_force() {
168 let env = FakeEnv {
169 vars: HashMap::from([
170 ("NO_COLOR".to_string(), "1".to_string()),
171 ("CLICOLOR_FORCE".to_string(), "1".to_string()),
172 ]),
173 ..FakeEnv::default()
174 };
175 assert!(!should_color(
176 ColorChoice::Auto,
177 false,
178 Stream::Stderr,
179 &env
180 ));
181 }
182
183 #[test]
184 fn auto_follows_tty_when_env_silent() {
185 let on = FakeEnv {
186 stderr_tty: true,
187 ..FakeEnv::default()
188 };
189 let off = FakeEnv::default();
190 assert!(should_color(ColorChoice::Auto, false, Stream::Stderr, &on));
191 assert!(!should_color(
192 ColorChoice::Auto,
193 false,
194 Stream::Stderr,
195 &off
196 ));
197 }
198}