use std::sync::OnceLock;
const GREEN: &str = "32";
const RED: &str = "31";
const YELLOW: &str = "33";
pub fn colors_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
let no_color = std::env::var_os("NO_COLOR")
.map(|v| !v.is_empty())
.unwrap_or(false);
let dumb = std::env::var("TERM").map(|t| t == "dumb").unwrap_or(false);
!no_color && !dumb
})
}
fn paint(text: &str, sgr: &str) -> String {
if colors_enabled() {
format!("\u{1b}[{sgr}m{text}\u{1b}[0m")
} else {
text.to_string()
}
}
fn sign(glyph: &str, sgr: &str) -> String {
format!(" {}", paint(glyph, sgr))
}
pub fn valid_sign() -> &'static str {
static S: OnceLock<String> = OnceLock::new();
S.get_or_init(|| sign("✓", GREEN))
}
pub fn error_sign() -> &'static str {
static S: OnceLock<String> = OnceLock::new();
S.get_or_init(|| sign("✗", RED))
}
pub fn warning_sign() -> &'static str {
static S: OnceLock<String> = OnceLock::new();
S.get_or_init(|| sign("!", YELLOW))
}
pub fn highlight(text: &str) -> String {
paint(&sanitize(text), YELLOW)
}
pub fn sanitize(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for c in text.chars() {
match c {
'\t' => out.push(' '),
c if (c as u32) < 0x20 || c == '\u{7f}' => {
out.push_str(&format!("\\x{:02x}", c as u32));
}
c if ('\u{80}'..='\u{9f}').contains(&c) => {
out.push_str(&format!("\\x{:02x}", c as u32));
}
'\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}' => {
out.push_str(&format!("\\u{{{:04x}}}", c as u32));
}
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_escapes_what_a_terminal_would_obey() {
assert_eq!(sanitize("a\u{1b}[8mb"), "a\\x1b[8mb");
assert_eq!(sanitize("bell\u{7}"), "bell\\x07");
assert_eq!(sanitize("cr\r"), "cr\\x0d");
assert_eq!(sanitize("del\u{7f}"), "del\\x7f");
assert_eq!(sanitize("csi\u{9b}"), "csi\\x9b");
assert_eq!(sanitize("rtl\u{202e}"), "rtl\\u{202e}");
assert_eq!(sanitize("iso\u{2066}"), "iso\\u{2066}");
assert_eq!(sanitize("a\tb"), "a b");
}
#[test]
fn sanitize_leaves_ordinary_text_alone() {
for s in ["plain ascii", "café", "日本語", "✓ ✗ !", "a/b-c_d.e", "🐛"] {
assert_eq!(sanitize(s), s, "{s:?} should pass through");
}
}
#[test]
fn no_color_is_honoured_per_the_standard() {
fn decide(no_color: Option<&str>, term: &str) -> bool {
let nc = no_color.map(|v| !v.is_empty()).unwrap_or(false);
!nc && term != "dumb"
}
assert!(decide(None, "xterm-256color"), "colour by default");
assert!(!decide(Some("1"), "xterm-256color"), "any value disables");
assert!(!decide(Some("0"), "xterm-256color"), "even \"0\" disables");
assert!(decide(Some(""), "xterm-256color"), "empty does NOT disable");
assert!(!decide(None, "dumb"), "TERM=dumb cannot render SGR");
}
}