ralph_workflow/logger/
mod.rs1#[cfg(any(test, feature = "test-utils"))]
56pub mod output;
57#[cfg(not(any(test, feature = "test-utils")))]
58mod output;
59
60mod progress;
61
62pub use output::{argv_requests_json, format_generic_json_for_display, Loggable, Logger};
63
64pub use progress::print_progress;
68
69use std::io::IsTerminal;
72
73pub fn colors_enabled() -> bool {
85 if std::env::var("NO_COLOR").is_ok() {
88 return false;
89 }
90
91 if let Ok(val) = std::env::var("CLICOLOR_FORCE") {
97 if !val.is_empty() && val != "0" {
98 return true;
99 }
100 }
101
102 if let Ok(val) = std::env::var("CLICOLOR") {
104 if val == "0" {
105 return false;
106 }
107 }
108
109 if let Ok(term) = std::env::var("TERM") {
111 if term.to_lowercase() == "dumb" {
112 return false;
113 }
114 }
115
116 std::io::stdout().is_terminal()
118}
119
120#[derive(Clone, Copy)]
122pub struct Colors {
123 pub(crate) enabled: bool,
124}
125
126impl Colors {
127 pub fn new() -> Self {
128 Self {
129 enabled: colors_enabled(),
130 }
131 }
132
133 pub const fn bold(self) -> &'static str {
135 if self.enabled {
136 "\x1b[1m"
137 } else {
138 ""
139 }
140 }
141
142 pub const fn dim(self) -> &'static str {
143 if self.enabled {
144 "\x1b[2m"
145 } else {
146 ""
147 }
148 }
149
150 pub const fn reset(self) -> &'static str {
151 if self.enabled {
152 "\x1b[0m"
153 } else {
154 ""
155 }
156 }
157
158 pub const fn red(self) -> &'static str {
160 if self.enabled {
161 "\x1b[31m"
162 } else {
163 ""
164 }
165 }
166
167 pub const fn green(self) -> &'static str {
168 if self.enabled {
169 "\x1b[32m"
170 } else {
171 ""
172 }
173 }
174
175 pub const fn yellow(self) -> &'static str {
176 if self.enabled {
177 "\x1b[33m"
178 } else {
179 ""
180 }
181 }
182
183 pub const fn blue(self) -> &'static str {
184 if self.enabled {
185 "\x1b[34m"
186 } else {
187 ""
188 }
189 }
190
191 pub const fn magenta(self) -> &'static str {
192 if self.enabled {
193 "\x1b[35m"
194 } else {
195 ""
196 }
197 }
198
199 pub const fn cyan(self) -> &'static str {
200 if self.enabled {
201 "\x1b[36m"
202 } else {
203 ""
204 }
205 }
206
207 pub const fn white(self) -> &'static str {
208 if self.enabled {
209 "\x1b[37m"
210 } else {
211 ""
212 }
213 }
214}
215
216impl Default for Colors {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222pub const BOX_TL: char = '╭';
224pub const BOX_TR: char = '╮';
225pub const BOX_BL: char = '╰';
226pub const BOX_BR: char = '╯';
227pub const BOX_H: char = '─';
228pub const BOX_V: char = '│';
229
230pub const ARROW: char = '→';
232pub const CHECK: char = '✓';
233pub const CROSS: char = '✗';
234pub const WARN: char = '⚠';
235pub const INFO: char = 'ℹ';
236
237#[cfg(test)]
238mod colors_tests {
239 use super::*;
240
241 #[test]
242 fn test_colors_disabled() {
243 let c = Colors { enabled: false };
244 assert_eq!(c.bold(), "");
245 assert_eq!(c.red(), "");
246 assert_eq!(c.reset(), "");
247 }
248
249 #[test]
250 fn test_colors_enabled() {
251 let c = Colors { enabled: true };
252 assert_eq!(c.bold(), "\x1b[1m");
253 assert_eq!(c.red(), "\x1b[31m");
254 assert_eq!(c.reset(), "\x1b[0m");
255 }
256
257 #[test]
258 fn test_box_chars() {
259 assert_eq!(BOX_TL, '╭');
260 assert_eq!(BOX_TR, '╮');
261 assert_eq!(BOX_H, '─');
262 }
263
264 #[test]
265 fn test_colors_enabled_respects_no_color() {
266 let original = std::env::var("NO_COLOR");
268
269 std::env::set_var("NO_COLOR", "1");
271
272 assert!(!colors_enabled(), "NO_COLOR=1 should disable colors");
274
275 match original {
277 Ok(val) => std::env::set_var("NO_COLOR", val),
278 Err(_) => std::env::remove_var("NO_COLOR"),
279 }
280 }
281
282 #[test]
283 fn test_colors_enabled_respects_clicolor_force() {
284 let original_no_color = std::env::var("NO_COLOR");
286 let original_clicolor_force = std::env::var("CLICOLOR_FORCE");
287
288 std::env::remove_var("NO_COLOR");
290
291 std::env::set_var("CLICOLOR_FORCE", "1");
293
294 assert!(colors_enabled(), "CLICOLOR_FORCE=1 should enable colors");
296
297 match original_no_color {
299 Ok(val) => std::env::set_var("NO_COLOR", val),
300 Err(_) => std::env::remove_var("NO_COLOR"),
301 }
302 match original_clicolor_force {
303 Ok(val) => std::env::set_var("CLICOLOR_FORCE", val),
304 Err(_) => std::env::remove_var("CLICOLOR_FORCE"),
305 }
306 }
307
308 #[test]
309 fn test_colors_enabled_respects_clicolor_zero() {
310 let original_no_color = std::env::var("NO_COLOR");
312 let original_clicolor = std::env::var("CLICOLOR");
313
314 std::env::remove_var("NO_COLOR");
316
317 std::env::set_var("CLICOLOR", "0");
319
320 assert!(!colors_enabled(), "CLICOLOR=0 should disable colors");
322
323 match original_no_color {
325 Ok(val) => std::env::set_var("NO_COLOR", val),
326 Err(_) => std::env::remove_var("NO_COLOR"),
327 }
328 match original_clicolor {
329 Ok(val) => std::env::set_var("CLICOLOR", val),
330 Err(_) => std::env::remove_var("CLICOLOR"),
331 }
332 }
333
334 #[test]
335 fn test_colors_enabled_respects_term_dumb() {
336 let original_no_color = std::env::var("NO_COLOR");
338 let original_term = std::env::var("TERM");
339
340 std::env::remove_var("NO_COLOR");
342
343 std::env::set_var("TERM", "dumb");
345
346 assert!(!colors_enabled(), "TERM=dumb should disable colors");
348
349 match original_no_color {
351 Ok(val) => std::env::set_var("NO_COLOR", val),
352 Err(_) => std::env::remove_var("NO_COLOR"),
353 }
354 match original_term {
355 Ok(val) => std::env::set_var("TERM", val),
356 Err(_) => std::env::remove_var("TERM"),
357 }
358 }
359
360 #[test]
361 fn test_colors_enabled_no_color_takes_precedence() {
362 let original_no_color = std::env::var("NO_COLOR");
364 let original_clicolor_force = std::env::var("CLICOLOR_FORCE");
365
366 std::env::set_var("NO_COLOR", "1");
368 std::env::set_var("CLICOLOR_FORCE", "1");
369
370 assert!(
372 !colors_enabled(),
373 "NO_COLOR should take precedence over CLICOLOR_FORCE"
374 );
375
376 match original_no_color {
378 Ok(val) => std::env::set_var("NO_COLOR", val),
379 Err(_) => std::env::remove_var("NO_COLOR"),
380 }
381 match original_clicolor_force {
382 Ok(val) => std::env::set_var("CLICOLOR_FORCE", val),
383 Err(_) => std::env::remove_var("CLICOLOR_FORCE"),
384 }
385 }
386
387 #[test]
388 fn test_colors_enabled_term_dumb_case_insensitive() {
389 let original_no_color = std::env::var("NO_COLOR");
391 let original_term = std::env::var("TERM");
392
393 std::env::remove_var("NO_COLOR");
395
396 for term_value in ["dumb", "DUMB", "Dumb", "DuMb"] {
398 std::env::set_var("TERM", term_value);
399 assert!(
400 !colors_enabled(),
401 "TERM={term_value} should disable colors (case-insensitive)"
402 );
403 }
404
405 match original_no_color {
407 Ok(val) => std::env::set_var("NO_COLOR", val),
408 Err(_) => std::env::remove_var("NO_COLOR"),
409 }
410 match original_term {
411 Ok(val) => std::env::set_var("TERM", val),
412 Err(_) => std::env::remove_var("TERM"),
413 }
414 }
415}