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 ansi;
18mod ansi_stripper;
19mod file_writer;
20mod io;
21mod logger_wrapper;
22mod progress;
23mod runtime;
24mod stdout_writer;
25
26pub use ansi::strip_ansi_codes;
27pub use output::{argv_requests_json, format_generic_json_for_display, Loggable, Logger};
28pub use progress::print_progress;
29
30pub use crate::logger::file_writer::append_to_file;
31pub use crate::logger::logger_wrapper::LoggerIoWrapper;
32
33pub trait ColorEnvironment {
34 fn get_var(&self, name: &str) -> Option<String>;
35 fn is_terminal(&self) -> bool;
36}
37
38pub struct RealColorEnvironment;
39
40impl ColorEnvironment for RealColorEnvironment {
41 fn get_var(&self, name: &str) -> Option<String> {
42 runtime::get_color_env_var(name)
43 }
44
45 fn is_terminal(&self) -> bool {
46 runtime_color_env_is_terminal()
47 }
48}
49
50pub fn stdout_write(buf: &[u8]) -> std::io::Result<usize> {
51 runtime::stdout_write(buf)
52}
53
54pub fn stdout_write_line(s: &str) -> std::io::Result<()> {
55 runtime::stdout_write_line(s)
56}
57
58pub fn stderr_write_line(s: &str) -> std::io::Result<()> {
59 runtime::stderr_write_line(s)
60}
61
62pub fn stdout_flush() -> std::io::Result<()> {
63 runtime::stdout_flush()
64}
65
66#[must_use]
67pub fn stdout_is_terminal() -> bool {
68 runtime::stdout_is_terminal()
69}
70
71fn runtime_color_env_is_terminal() -> bool {
72 let env = runtime::RealColorEnvironment;
73 let _ = runtime::ColorEnvironment::get_var(&env, "TERM");
74 runtime::ColorEnvironment::is_terminal(&env)
75}
76
77pub fn colors_enabled_with_env(env: &dyn ColorEnvironment) -> bool {
81 if env.get_var("NO_COLOR").is_some() {
84 return false;
85 }
86
87 if let Some(val) = env.get_var("CLICOLOR_FORCE") {
90 if !val.is_empty() && val != "0" {
91 return true;
92 }
93 }
94
95 if let Some(val) = env.get_var("CLICOLOR") {
97 if val == "0" {
98 return false;
99 }
100 }
101
102 if let Some(term) = env.get_var("TERM") {
104 if term.to_lowercase() == "dumb" {
105 return false;
106 }
107 }
108
109 env.is_terminal()
111}
112
113#[must_use]
121pub fn colors_enabled() -> bool {
122 colors_enabled_with_env(&RealColorEnvironment)
123}
124
125#[derive(Clone, Copy)]
127pub struct Colors {
128 pub(crate) enabled: bool,
129}
130
131impl Colors {
132 #[must_use]
133 pub fn new() -> Self {
134 Self {
135 enabled: colors_enabled(),
136 }
137 }
138
139 #[cfg(any(test, feature = "test-utils"))]
168 #[must_use]
169 pub const fn with_enabled(enabled: bool) -> Self {
170 Self { enabled }
171 }
172
173 #[must_use]
174 pub const fn bold(self) -> &'static str {
175 if self.enabled {
176 "\x1b[1m"
177 } else {
178 ""
179 }
180 }
181
182 #[must_use]
183 pub const fn dim(self) -> &'static str {
184 if self.enabled {
185 "\x1b[2m"
186 } else {
187 ""
188 }
189 }
190
191 #[must_use]
192 pub const fn reset(self) -> &'static str {
193 if self.enabled {
194 "\x1b[0m"
195 } else {
196 ""
197 }
198 }
199
200 #[must_use]
201 pub const fn red(self) -> &'static str {
202 if self.enabled {
203 "\x1b[31m"
204 } else {
205 ""
206 }
207 }
208
209 #[must_use]
210 pub const fn green(self) -> &'static str {
211 if self.enabled {
212 "\x1b[32m"
213 } else {
214 ""
215 }
216 }
217
218 #[must_use]
219 pub const fn yellow(self) -> &'static str {
220 if self.enabled {
221 "\x1b[33m"
222 } else {
223 ""
224 }
225 }
226
227 #[must_use]
228 pub const fn blue(self) -> &'static str {
229 if self.enabled {
230 "\x1b[34m"
231 } else {
232 ""
233 }
234 }
235
236 #[must_use]
237 pub const fn magenta(self) -> &'static str {
238 if self.enabled {
239 "\x1b[35m"
240 } else {
241 ""
242 }
243 }
244
245 #[must_use]
246 pub const fn cyan(self) -> &'static str {
247 if self.enabled {
248 "\x1b[36m"
249 } else {
250 ""
251 }
252 }
253
254 #[must_use]
255 pub const fn white(self) -> &'static str {
256 if self.enabled {
257 "\x1b[37m"
258 } else {
259 ""
260 }
261 }
262}
263
264impl Default for Colors {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270pub const BOX_TL: char = '╭';
272pub const BOX_TR: char = '╮';
273pub const BOX_BL: char = '╰';
274pub const BOX_BR: char = '╯';
275pub const BOX_H: char = '─';
276pub const BOX_V: char = '│';
277
278pub const ARROW: char = '→';
280pub const CHECK: char = '✓';
281pub const CROSS: char = '✗';
282pub const WARN: char = '⚠';
283pub const INFO: char = 'ℹ';
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use std::collections::HashMap;
289
290 struct MockColorEnvironment {
292 vars: HashMap<String, String>,
293 is_tty: bool,
294 }
295
296 impl MockColorEnvironment {
297 fn new() -> Self {
298 Self {
299 vars: HashMap::new(),
300 is_tty: true,
301 }
302 }
303
304 fn with_var(mut self, name: &str, value: &str) -> Self {
305 self.vars.insert(name.to_string(), value.to_string());
306 self
307 }
308
309 fn not_tty(mut self) -> Self {
310 self.is_tty = false;
311 self
312 }
313 }
314
315 impl ColorEnvironment for MockColorEnvironment {
316 fn get_var(&self, name: &str) -> Option<String> {
317 self.vars.get(name).cloned()
318 }
319
320 fn is_terminal(&self) -> bool {
321 self.is_tty
322 }
323 }
324
325 #[test]
326 fn test_colors_disabled_struct() {
327 let c = Colors { enabled: false };
328 assert_eq!(c.bold(), "");
329 assert_eq!(c.red(), "");
330 assert_eq!(c.reset(), "");
331 }
332
333 #[test]
334 fn test_colors_enabled_struct() {
335 let c = Colors { enabled: true };
336 assert_eq!(c.bold(), "\x1b[1m");
337 assert_eq!(c.red(), "\x1b[31m");
338 assert_eq!(c.reset(), "\x1b[0m");
339 }
340
341 #[test]
342 fn test_box_chars() {
343 assert_eq!(BOX_TL, '╭');
344 assert_eq!(BOX_TR, '╮');
345 assert_eq!(BOX_H, '─');
346 }
347
348 #[test]
349 fn test_colors_enabled_respects_no_color() {
350 let env = MockColorEnvironment::new().with_var("NO_COLOR", "1");
351 assert!(!colors_enabled_with_env(&env));
352 }
353
354 #[test]
355 fn test_colors_enabled_respects_clicolor_force() {
356 let env = MockColorEnvironment::new()
357 .with_var("CLICOLOR_FORCE", "1")
358 .not_tty();
359 assert!(colors_enabled_with_env(&env));
360 }
361
362 #[test]
363 fn test_colors_enabled_respects_clicolor_zero() {
364 let env = MockColorEnvironment::new().with_var("CLICOLOR", "0");
365 assert!(!colors_enabled_with_env(&env));
366 }
367
368 #[test]
369 fn test_colors_enabled_respects_term_dumb() {
370 let env = MockColorEnvironment::new().with_var("TERM", "dumb");
371 assert!(!colors_enabled_with_env(&env));
372 }
373
374 #[test]
375 fn test_colors_enabled_no_color_takes_precedence() {
376 let env = MockColorEnvironment::new()
377 .with_var("NO_COLOR", "1")
378 .with_var("CLICOLOR_FORCE", "1");
379 assert!(!colors_enabled_with_env(&env));
380 }
381
382 #[test]
383 fn test_colors_enabled_term_dumb_case_insensitive() {
384 assert!(["dumb", "DUMB", "Dumb", "DuMb"].iter().all(|&term| {
385 let env = MockColorEnvironment::new().with_var("TERM", term);
386 !colors_enabled_with_env(&env)
387 }));
388 }
389
390 #[test]
391 fn test_colors_enabled_default_tty() {
392 let env = MockColorEnvironment::new();
393 assert!(colors_enabled_with_env(&env));
394 }
395
396 #[test]
397 fn test_colors_enabled_default_not_tty() {
398 let env = MockColorEnvironment::new().not_tty();
399 assert!(!colors_enabled_with_env(&env));
400 }
401}