ralph_workflow/logger/
mod.rs1mod output;
26mod progress;
27
28pub use output::{argv_requests_json, format_generic_json_for_display, Logger};
29pub use progress::print_progress;
30
31use std::io::IsTerminal;
34
35pub fn colors_enabled() -> bool {
47 if std::env::var("NO_COLOR").is_ok() {
50 return false;
51 }
52
53 if let Ok(val) = std::env::var("CLICOLOR_FORCE") {
59 if !val.is_empty() && val != "0" {
60 return true;
61 }
62 }
63
64 if let Ok(val) = std::env::var("CLICOLOR") {
66 if val == "0" {
67 return false;
68 }
69 }
70
71 if let Ok(term) = std::env::var("TERM") {
73 if term.to_lowercase() == "dumb" {
74 return false;
75 }
76 }
77
78 std::io::stdout().is_terminal()
80}
81
82#[derive(Clone, Copy)]
84pub struct Colors {
85 pub(crate) enabled: bool,
86}
87
88impl Colors {
89 pub fn new() -> Self {
90 Self {
91 enabled: colors_enabled(),
92 }
93 }
94
95 pub const fn bold(self) -> &'static str {
97 if self.enabled {
98 "\x1b[1m"
99 } else {
100 ""
101 }
102 }
103
104 pub const fn dim(self) -> &'static str {
105 if self.enabled {
106 "\x1b[2m"
107 } else {
108 ""
109 }
110 }
111
112 pub const fn reset(self) -> &'static str {
113 if self.enabled {
114 "\x1b[0m"
115 } else {
116 ""
117 }
118 }
119
120 pub const fn red(self) -> &'static str {
122 if self.enabled {
123 "\x1b[31m"
124 } else {
125 ""
126 }
127 }
128
129 pub const fn green(self) -> &'static str {
130 if self.enabled {
131 "\x1b[32m"
132 } else {
133 ""
134 }
135 }
136
137 pub const fn yellow(self) -> &'static str {
138 if self.enabled {
139 "\x1b[33m"
140 } else {
141 ""
142 }
143 }
144
145 pub const fn blue(self) -> &'static str {
146 if self.enabled {
147 "\x1b[34m"
148 } else {
149 ""
150 }
151 }
152
153 pub const fn magenta(self) -> &'static str {
154 if self.enabled {
155 "\x1b[35m"
156 } else {
157 ""
158 }
159 }
160
161 pub const fn cyan(self) -> &'static str {
162 if self.enabled {
163 "\x1b[36m"
164 } else {
165 ""
166 }
167 }
168
169 pub const fn white(self) -> &'static str {
170 if self.enabled {
171 "\x1b[37m"
172 } else {
173 ""
174 }
175 }
176}
177
178impl Default for Colors {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184pub const BOX_TL: char = '╭';
186pub const BOX_TR: char = '╮';
187pub const BOX_BL: char = '╰';
188pub const BOX_BR: char = '╯';
189pub const BOX_H: char = '─';
190pub const BOX_V: char = '│';
191
192pub const ARROW: char = '→';
194pub const CHECK: char = '✓';
195pub const CROSS: char = '✗';
196pub const WARN: char = '⚠';
197pub const INFO: char = 'ℹ';
198
199#[cfg(test)]
200mod colors_tests {
201 use super::*;
202
203 #[test]
204 fn test_colors_disabled() {
205 let c = Colors { enabled: false };
206 assert_eq!(c.bold(), "");
207 assert_eq!(c.red(), "");
208 assert_eq!(c.reset(), "");
209 }
210
211 #[test]
212 fn test_colors_enabled() {
213 let c = Colors { enabled: true };
214 assert_eq!(c.bold(), "\x1b[1m");
215 assert_eq!(c.red(), "\x1b[31m");
216 assert_eq!(c.reset(), "\x1b[0m");
217 }
218
219 #[test]
220 fn test_box_chars() {
221 assert_eq!(BOX_TL, '╭');
222 assert_eq!(BOX_TR, '╮');
223 assert_eq!(BOX_H, '─');
224 }
225
226 #[test]
227 fn test_colors_enabled_respects_no_color() {
228 let original = std::env::var("NO_COLOR");
230
231 std::env::set_var("NO_COLOR", "1");
233
234 assert!(!colors_enabled(), "NO_COLOR=1 should disable colors");
236
237 match original {
239 Ok(val) => std::env::set_var("NO_COLOR", val),
240 Err(_) => std::env::remove_var("NO_COLOR"),
241 }
242 }
243
244 #[test]
245 fn test_colors_enabled_respects_clicolor_force() {
246 let original_no_color = std::env::var("NO_COLOR");
248 let original_clicolor_force = std::env::var("CLICOLOR_FORCE");
249
250 std::env::remove_var("NO_COLOR");
252
253 std::env::set_var("CLICOLOR_FORCE", "1");
255
256 assert!(colors_enabled(), "CLICOLOR_FORCE=1 should enable colors");
258
259 match original_no_color {
261 Ok(val) => std::env::set_var("NO_COLOR", val),
262 Err(_) => std::env::remove_var("NO_COLOR"),
263 }
264 match original_clicolor_force {
265 Ok(val) => std::env::set_var("CLICOLOR_FORCE", val),
266 Err(_) => std::env::remove_var("CLICOLOR_FORCE"),
267 }
268 }
269
270 #[test]
271 fn test_colors_enabled_respects_clicolor_zero() {
272 let original_no_color = std::env::var("NO_COLOR");
274 let original_clicolor = std::env::var("CLICOLOR");
275
276 std::env::remove_var("NO_COLOR");
278
279 std::env::set_var("CLICOLOR", "0");
281
282 assert!(!colors_enabled(), "CLICOLOR=0 should disable colors");
284
285 match original_no_color {
287 Ok(val) => std::env::set_var("NO_COLOR", val),
288 Err(_) => std::env::remove_var("NO_COLOR"),
289 }
290 match original_clicolor {
291 Ok(val) => std::env::set_var("CLICOLOR", val),
292 Err(_) => std::env::remove_var("CLICOLOR"),
293 }
294 }
295
296 #[test]
297 fn test_colors_enabled_respects_term_dumb() {
298 let original_no_color = std::env::var("NO_COLOR");
300 let original_term = std::env::var("TERM");
301
302 std::env::remove_var("NO_COLOR");
304
305 std::env::set_var("TERM", "dumb");
307
308 assert!(!colors_enabled(), "TERM=dumb should disable colors");
310
311 match original_no_color {
313 Ok(val) => std::env::set_var("NO_COLOR", val),
314 Err(_) => std::env::remove_var("NO_COLOR"),
315 }
316 match original_term {
317 Ok(val) => std::env::set_var("TERM", val),
318 Err(_) => std::env::remove_var("TERM"),
319 }
320 }
321
322 #[test]
323 fn test_colors_enabled_no_color_takes_precedence() {
324 let original_no_color = std::env::var("NO_COLOR");
326 let original_clicolor_force = std::env::var("CLICOLOR_FORCE");
327
328 std::env::set_var("NO_COLOR", "1");
330 std::env::set_var("CLICOLOR_FORCE", "1");
331
332 assert!(
334 !colors_enabled(),
335 "NO_COLOR should take precedence over CLICOLOR_FORCE"
336 );
337
338 match original_no_color {
340 Ok(val) => std::env::set_var("NO_COLOR", val),
341 Err(_) => std::env::remove_var("NO_COLOR"),
342 }
343 match original_clicolor_force {
344 Ok(val) => std::env::set_var("CLICOLOR_FORCE", val),
345 Err(_) => std::env::remove_var("CLICOLOR_FORCE"),
346 }
347 }
348
349 #[test]
350 fn test_colors_enabled_term_dumb_case_insensitive() {
351 let original_no_color = std::env::var("NO_COLOR");
353 let original_term = std::env::var("TERM");
354
355 std::env::remove_var("NO_COLOR");
357
358 for term_value in ["dumb", "DUMB", "Dumb", "DuMb"] {
360 std::env::set_var("TERM", term_value);
361 assert!(
362 !colors_enabled(),
363 "TERM={term_value} should disable colors (case-insensitive)"
364 );
365 }
366
367 match original_no_color {
369 Ok(val) => std::env::set_var("NO_COLOR", val),
370 Err(_) => std::env::remove_var("NO_COLOR"),
371 }
372 match original_term {
373 Ok(val) => std::env::set_var("TERM", val),
374 Err(_) => std::env::remove_var("TERM"),
375 }
376 }
377}