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
86#[must_use]
94pub fn colors_enabled() -> bool {
95 colors_enabled_with_env(&RealColorEnvironment)
96}
97
98#[derive(Clone, Copy)]
100pub struct Colors {
101 pub(crate) enabled: bool,
102}
103
104impl Colors {
105 #[must_use]
106 pub fn new() -> Self {
107 Self {
108 enabled: colors_enabled(),
109 }
110 }
111
112 #[cfg(any(test, feature = "test-utils"))]
141 #[must_use]
142 pub const fn with_enabled(enabled: bool) -> Self {
143 Self { enabled }
144 }
145
146 #[must_use]
147 pub const fn bold(self) -> &'static str {
148 if self.enabled {
149 "\x1b[1m"
150 } else {
151 ""
152 }
153 }
154
155 #[must_use]
156 pub const fn dim(self) -> &'static str {
157 if self.enabled {
158 "\x1b[2m"
159 } else {
160 ""
161 }
162 }
163
164 #[must_use]
165 pub const fn reset(self) -> &'static str {
166 if self.enabled {
167 "\x1b[0m"
168 } else {
169 ""
170 }
171 }
172
173 #[must_use]
174 pub const fn red(self) -> &'static str {
175 if self.enabled {
176 "\x1b[31m"
177 } else {
178 ""
179 }
180 }
181
182 #[must_use]
183 pub const fn green(self) -> &'static str {
184 if self.enabled {
185 "\x1b[32m"
186 } else {
187 ""
188 }
189 }
190
191 #[must_use]
192 pub const fn yellow(self) -> &'static str {
193 if self.enabled {
194 "\x1b[33m"
195 } else {
196 ""
197 }
198 }
199
200 #[must_use]
201 pub const fn blue(self) -> &'static str {
202 if self.enabled {
203 "\x1b[34m"
204 } else {
205 ""
206 }
207 }
208
209 #[must_use]
210 pub const fn magenta(self) -> &'static str {
211 if self.enabled {
212 "\x1b[35m"
213 } else {
214 ""
215 }
216 }
217
218 #[must_use]
219 pub const fn cyan(self) -> &'static str {
220 if self.enabled {
221 "\x1b[36m"
222 } else {
223 ""
224 }
225 }
226
227 #[must_use]
228 pub const fn white(self) -> &'static str {
229 if self.enabled {
230 "\x1b[37m"
231 } else {
232 ""
233 }
234 }
235}
236
237impl Default for Colors {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243pub const BOX_TL: char = '╭';
245pub const BOX_TR: char = '╮';
246pub const BOX_BL: char = '╰';
247pub const BOX_BR: char = '╯';
248pub const BOX_H: char = '─';
249pub const BOX_V: char = '│';
250
251pub const ARROW: char = '→';
253pub const CHECK: char = '✓';
254pub const CROSS: char = '✗';
255pub const WARN: char = '⚠';
256pub const INFO: char = 'ℹ';
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use std::collections::HashMap;
262
263 struct MockColorEnvironment {
265 vars: HashMap<String, String>,
266 is_tty: bool,
267 }
268
269 impl MockColorEnvironment {
270 fn new() -> Self {
271 Self {
272 vars: HashMap::new(),
273 is_tty: true,
274 }
275 }
276
277 fn with_var(mut self, name: &str, value: &str) -> Self {
278 self.vars.insert(name.to_string(), value.to_string());
279 self
280 }
281
282 fn not_tty(mut self) -> Self {
283 self.is_tty = false;
284 self
285 }
286 }
287
288 impl ColorEnvironment for MockColorEnvironment {
289 fn get_var(&self, name: &str) -> Option<String> {
290 self.vars.get(name).cloned()
291 }
292
293 fn is_terminal(&self) -> bool {
294 self.is_tty
295 }
296 }
297
298 #[test]
299 fn test_colors_disabled_struct() {
300 let c = Colors { enabled: false };
301 assert_eq!(c.bold(), "");
302 assert_eq!(c.red(), "");
303 assert_eq!(c.reset(), "");
304 }
305
306 #[test]
307 fn test_colors_enabled_struct() {
308 let c = Colors { enabled: true };
309 assert_eq!(c.bold(), "\x1b[1m");
310 assert_eq!(c.red(), "\x1b[31m");
311 assert_eq!(c.reset(), "\x1b[0m");
312 }
313
314 #[test]
315 fn test_box_chars() {
316 assert_eq!(BOX_TL, '╭');
317 assert_eq!(BOX_TR, '╮');
318 assert_eq!(BOX_H, '─');
319 }
320
321 #[test]
322 fn test_colors_enabled_respects_no_color() {
323 let env = MockColorEnvironment::new().with_var("NO_COLOR", "1");
324 assert!(!colors_enabled_with_env(&env));
325 }
326
327 #[test]
328 fn test_colors_enabled_respects_clicolor_force() {
329 let env = MockColorEnvironment::new()
330 .with_var("CLICOLOR_FORCE", "1")
331 .not_tty();
332 assert!(colors_enabled_with_env(&env));
333 }
334
335 #[test]
336 fn test_colors_enabled_respects_clicolor_zero() {
337 let env = MockColorEnvironment::new().with_var("CLICOLOR", "0");
338 assert!(!colors_enabled_with_env(&env));
339 }
340
341 #[test]
342 fn test_colors_enabled_respects_term_dumb() {
343 let env = MockColorEnvironment::new().with_var("TERM", "dumb");
344 assert!(!colors_enabled_with_env(&env));
345 }
346
347 #[test]
348 fn test_colors_enabled_no_color_takes_precedence() {
349 let env = MockColorEnvironment::new()
350 .with_var("NO_COLOR", "1")
351 .with_var("CLICOLOR_FORCE", "1");
352 assert!(!colors_enabled_with_env(&env));
353 }
354
355 #[test]
356 fn test_colors_enabled_term_dumb_case_insensitive() {
357 for term in ["dumb", "DUMB", "Dumb", "DuMb"] {
358 let env = MockColorEnvironment::new().with_var("TERM", term);
359 assert!(
360 !colors_enabled_with_env(&env),
361 "TERM={term} should disable colors"
362 );
363 }
364 }
365
366 #[test]
367 fn test_colors_enabled_default_tty() {
368 let env = MockColorEnvironment::new();
369 assert!(colors_enabled_with_env(&env));
370 }
371
372 #[test]
373 fn test_colors_enabled_default_not_tty() {
374 let env = MockColorEnvironment::new().not_tty();
375 assert!(!colors_enabled_with_env(&env));
376 }
377}