Add a centralized color module (cli/src/color.rs) that respects the NO_COLOR environment variable per https://no-color.org/ Changes: - Add color.rs module with helper functions for colored output - Refactor all hardcoded ANSI escape codes to use the color module - Add tests for color formatting functions - Update AGENTS.md with color module usage guidelines Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
159 lines
4.0 KiB
Rust
159 lines
4.0 KiB
Rust
//! Color output utilities respecting NO_COLOR environment variable.
|
|
//!
|
|
//! When the NO_COLOR environment variable is present (regardless of value),
|
|
//! all color formatting is disabled per https://no-color.org/
|
|
|
|
use std::env;
|
|
use std::sync::OnceLock;
|
|
|
|
/// Returns true if color output is enabled (NO_COLOR is NOT set)
|
|
pub fn is_enabled() -> bool {
|
|
static COLORS_ENABLED: OnceLock<bool> = OnceLock::new();
|
|
*COLORS_ENABLED.get_or_init(|| env::var("NO_COLOR").is_err())
|
|
}
|
|
|
|
/// Format text in red (errors)
|
|
pub fn red(text: &str) -> String {
|
|
if is_enabled() {
|
|
format!("\x1b[31m{}\x1b[0m", text)
|
|
} else {
|
|
text.to_string()
|
|
}
|
|
}
|
|
|
|
/// Format text in green (success)
|
|
pub fn green(text: &str) -> String {
|
|
if is_enabled() {
|
|
format!("\x1b[32m{}\x1b[0m", text)
|
|
} else {
|
|
text.to_string()
|
|
}
|
|
}
|
|
|
|
/// Format text in yellow (warnings)
|
|
pub fn yellow(text: &str) -> String {
|
|
if is_enabled() {
|
|
format!("\x1b[33m{}\x1b[0m", text)
|
|
} else {
|
|
text.to_string()
|
|
}
|
|
}
|
|
|
|
/// Format text in cyan (info/progress)
|
|
pub fn cyan(text: &str) -> String {
|
|
if is_enabled() {
|
|
format!("\x1b[36m{}\x1b[0m", text)
|
|
} else {
|
|
text.to_string()
|
|
}
|
|
}
|
|
|
|
/// Format text in bold
|
|
pub fn bold(text: &str) -> String {
|
|
if is_enabled() {
|
|
format!("\x1b[1m{}\x1b[0m", text)
|
|
} else {
|
|
text.to_string()
|
|
}
|
|
}
|
|
|
|
/// Format text in dim
|
|
pub fn dim(text: &str) -> String {
|
|
if is_enabled() {
|
|
format!("\x1b[2m{}\x1b[0m", text)
|
|
} else {
|
|
text.to_string()
|
|
}
|
|
}
|
|
|
|
/// Red X error indicator
|
|
pub fn error_indicator() -> &'static str {
|
|
static INDICATOR: OnceLock<String> = OnceLock::new();
|
|
INDICATOR.get_or_init(|| {
|
|
if is_enabled() {
|
|
"\x1b[31m✗\x1b[0m".to_string()
|
|
} else {
|
|
"✗".to_string()
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Green checkmark success indicator
|
|
pub fn success_indicator() -> &'static str {
|
|
static INDICATOR: OnceLock<String> = OnceLock::new();
|
|
INDICATOR.get_or_init(|| {
|
|
if is_enabled() {
|
|
"\x1b[32m✓\x1b[0m".to_string()
|
|
} else {
|
|
"✓".to_string()
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Yellow warning indicator
|
|
pub fn warning_indicator() -> &'static str {
|
|
static INDICATOR: OnceLock<String> = OnceLock::new();
|
|
INDICATOR.get_or_init(|| {
|
|
if is_enabled() {
|
|
"\x1b[33m⚠\x1b[0m".to_string()
|
|
} else {
|
|
"⚠".to_string()
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Get console log color prefix by level
|
|
pub fn console_level_prefix(level: &str) -> String {
|
|
if !is_enabled() {
|
|
return format!("[{}]", level);
|
|
}
|
|
|
|
let color = match level {
|
|
"error" => "\x1b[31m",
|
|
"warning" => "\x1b[33m",
|
|
"info" => "\x1b[36m",
|
|
_ => "",
|
|
};
|
|
if color.is_empty() {
|
|
format!("[{}]", level)
|
|
} else {
|
|
format!("{}[{}]\x1b[0m", color, level)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_red_contains_ansi_codes() {
|
|
// Test the format structure (actual color depends on NO_COLOR env)
|
|
let formatted = format!("\x1b[31m{}\x1b[0m", "error");
|
|
assert!(formatted.contains("\x1b[31m"));
|
|
assert!(formatted.contains("\x1b[0m"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_green_contains_ansi_codes() {
|
|
let formatted = format!("\x1b[32m{}\x1b[0m", "success");
|
|
assert!(formatted.contains("\x1b[32m"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_console_level_prefix_contains_level() {
|
|
// Regardless of color state, the level text should be present
|
|
assert!(console_level_prefix("error").contains("error"));
|
|
assert!(console_level_prefix("warning").contains("warning"));
|
|
assert!(console_level_prefix("info").contains("info"));
|
|
assert!(console_level_prefix("log").contains("log"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_indicators_contain_symbols() {
|
|
// Regardless of color state, symbols should be present
|
|
assert!(error_indicator().contains('✗'));
|
|
assert!(success_indicator().contains('✓'));
|
|
assert!(warning_indicator().contains('⚠'));
|
|
}
|
|
}
|