ralph_workflow/logger/
mod.rs1#[cfg(any(test, feature = "test-utils"))]
13pub mod output;
14#[cfg(not(any(test, feature = "test-utils")))]
15mod output;
16
17mod progress;
18
19pub use output::{argv_requests_json, format_generic_json_for_display, Loggable, Logger};
20pub use progress::print_progress;
21
22use std::io::IsTerminal;
25
26pub trait ColorEnvironment {
31 fn get_var(&self, name: &str) -> Option<String>;
33 fn is_terminal(&self) -> bool;
35}
36
37pub struct RealColorEnvironment;
39
40impl ColorEnvironment for RealColorEnvironment {
41 fn get_var(&self, name: &str) -> Option<String> {
42 std::env::var(name).ok()
43 }
44
45 fn is_terminal(&self) -> bool {
46 std::io::stdout().is_terminal()
47 }
48}
49
50pub fn colors_enabled_with_env(env: &dyn ColorEnvironment) -> bool {
54 if env.get_var("NO_COLOR").is_some() {
57 return false;
58 }
59
60 if let Some(val) = env.get_var("CLICOLOR_FORCE") {
63 if !val.is_empty() && val != "0" {
64 return true;
65 }
66 }
67
68 if let Some(val) = env.get_var("CLICOLOR") {
70 if val == "0" {
71 return false;
72 }
73 }
74
75 if let Some(term) = env.get_var("TERM") {
77 if term.to_lowercase() == "dumb" {
78 return false;
79 }
80 }
81
82 env.is_terminal()
84}
85
86pub fn colors_enabled() -> bool {
94 colors_enabled_with_env(&RealColorEnvironment)
95}
96
97#[derive(Clone, Copy)]
99pub struct Colors {
100 pub(crate) enabled: bool,
101}
102
103impl Colors {
104 pub fn new() -> Self {
105 Self {
106 enabled: colors_enabled(),
107 }
108 }
109
110 #[cfg(any(test, feature = "test-utils"))]
139 pub const fn with_enabled(enabled: bool) -> Self {
140 Self { enabled }
141 }
142
143 pub const fn bold(self) -> &'static str {
144 if self.enabled {
145 "\x1b[1m"
146 } else {
147 ""
148 }
149 }
150
151 pub const fn dim(self) -> &'static str {
152 if self.enabled {
153 "\x1b[2m"
154 } else {
155 ""
156 }
157 }
158
159 pub const fn reset(self) -> &'static str {
160 if self.enabled {
161 "\x1b[0m"
162 } else {
163 ""
164 }
165 }
166
167 pub const fn red(self) -> &'static str {
168 if self.enabled {
169 "\x1b[31m"
170 } else {
171 ""
172 }
173 }
174
175 pub const fn green(self) -> &'static str {
176 if self.enabled {
177 "\x1b[32m"
178 } else {
179 ""
180 }
181 }
182
183 pub const fn yellow(self) -> &'static str {
184 if self.enabled {
185 "\x1b[33m"
186 } else {
187 ""
188 }
189 }
190
191 pub const fn blue(self) -> &'static str {
192 if self.enabled {
193 "\x1b[34m"
194 } else {
195 ""
196 }
197 }
198
199 pub const fn magenta(self) -> &'static str {
200 if self.enabled {
201 "\x1b[35m"
202 } else {
203 ""
204 }
205 }
206
207 pub const fn cyan(self) -> &'static str {
208 if self.enabled {
209 "\x1b[36m"
210 } else {
211 ""
212 }
213 }
214
215 pub const fn white(self) -> &'static str {
216 if self.enabled {
217 "\x1b[37m"
218 } else {
219 ""
220 }
221 }
222}
223
224impl Default for Colors {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230pub const BOX_TL: char = '╭';
232pub const BOX_TR: char = '╮';
233pub const BOX_BL: char = '╰';
234pub const BOX_BR: char = '╯';
235pub const BOX_H: char = '─';
236pub const BOX_V: char = '│';
237
238pub const ARROW: char = '→';
240pub const CHECK: char = '✓';
241pub const CROSS: char = '✗';
242pub const WARN: char = '⚠';
243pub const INFO: char = 'ℹ';
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use std::collections::HashMap;
249
250 struct MockColorEnvironment {
252 vars: HashMap<String, String>,
253 is_tty: bool,
254 }
255
256 impl MockColorEnvironment {
257 fn new() -> Self {
258 Self {
259 vars: HashMap::new(),
260 is_tty: true,
261 }
262 }
263
264 fn with_var(mut self, name: &str, value: &str) -> Self {
265 self.vars.insert(name.to_string(), value.to_string());
266 self
267 }
268
269 fn not_tty(mut self) -> Self {
270 self.is_tty = false;
271 self
272 }
273 }
274
275 impl ColorEnvironment for MockColorEnvironment {
276 fn get_var(&self, name: &str) -> Option<String> {
277 self.vars.get(name).cloned()
278 }
279
280 fn is_terminal(&self) -> bool {
281 self.is_tty
282 }
283 }
284
285 #[test]
286 fn test_colors_disabled_struct() {
287 let c = Colors { enabled: false };
288 assert_eq!(c.bold(), "");
289 assert_eq!(c.red(), "");
290 assert_eq!(c.reset(), "");
291 }
292
293 #[test]
294 fn test_colors_enabled_struct() {
295 let c = Colors { enabled: true };
296 assert_eq!(c.bold(), "\x1b[1m");
297 assert_eq!(c.red(), "\x1b[31m");
298 assert_eq!(c.reset(), "\x1b[0m");
299 }
300
301 #[test]
302 fn test_box_chars() {
303 assert_eq!(BOX_TL, '╭');
304 assert_eq!(BOX_TR, '╮');
305 assert_eq!(BOX_H, '─');
306 }
307
308 #[test]
309 fn test_colors_enabled_respects_no_color() {
310 let env = MockColorEnvironment::new().with_var("NO_COLOR", "1");
311 assert!(!colors_enabled_with_env(&env));
312 }
313
314 #[test]
315 fn test_colors_enabled_respects_clicolor_force() {
316 let env = MockColorEnvironment::new()
317 .with_var("CLICOLOR_FORCE", "1")
318 .not_tty();
319 assert!(colors_enabled_with_env(&env));
320 }
321
322 #[test]
323 fn test_colors_enabled_respects_clicolor_zero() {
324 let env = MockColorEnvironment::new().with_var("CLICOLOR", "0");
325 assert!(!colors_enabled_with_env(&env));
326 }
327
328 #[test]
329 fn test_colors_enabled_respects_term_dumb() {
330 let env = MockColorEnvironment::new().with_var("TERM", "dumb");
331 assert!(!colors_enabled_with_env(&env));
332 }
333
334 #[test]
335 fn test_colors_enabled_no_color_takes_precedence() {
336 let env = MockColorEnvironment::new()
337 .with_var("NO_COLOR", "1")
338 .with_var("CLICOLOR_FORCE", "1");
339 assert!(!colors_enabled_with_env(&env));
340 }
341
342 #[test]
343 fn test_colors_enabled_term_dumb_case_insensitive() {
344 for term in ["dumb", "DUMB", "Dumb", "DuMb"] {
345 let env = MockColorEnvironment::new().with_var("TERM", term);
346 assert!(
347 !colors_enabled_with_env(&env),
348 "TERM={term} should disable colors"
349 );
350 }
351 }
352
353 #[test]
354 fn test_colors_enabled_default_tty() {
355 let env = MockColorEnvironment::new();
356 assert!(colors_enabled_with_env(&env));
357 }
358
359 #[test]
360 fn test_colors_enabled_default_not_tty() {
361 let env = MockColorEnvironment::new().not_tty();
362 assert!(!colors_enabled_with_env(&env));
363 }
364}