use std::env;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ColorDepth {
Truecolor,
Color256,
#[default]
Color16,
}
impl ColorDepth {
pub fn detect() -> Self {
if !is_stdout_tty() {
return Self::Color16;
}
match env::var("COLORTERM").as_deref() {
Ok("truecolor") | Ok("24bit") => Self::Truecolor,
_ => Self::Color16,
}
}
}
pub fn resolve_depth(
requested: ColorDepth,
detected: ColorDepth,
suppress_warning: bool,
) -> ColorDepth {
let (result, downgraded) = match (requested, detected) {
(ColorDepth::Truecolor, ColorDepth::Truecolor) => (ColorDepth::Truecolor, false),
(ColorDepth::Truecolor, ColorDepth::Color256) => (ColorDepth::Color256, true),
(ColorDepth::Truecolor, ColorDepth::Color16) => (ColorDepth::Color16, true),
(ColorDepth::Color256, ColorDepth::Truecolor) => (ColorDepth::Color256, false),
(ColorDepth::Color256, ColorDepth::Color256) => (ColorDepth::Color256, false),
(ColorDepth::Color256, ColorDepth::Color16) => (ColorDepth::Color16, true),
(ColorDepth::Color16, _) => (ColorDepth::Color16, false),
};
if downgraded && !suppress_warning {
emit_downgrade_warning();
}
result
}
#[cold]
#[inline(never)]
fn emit_downgrade_warning() {
eprintln!(
"rusty-figlet: requested color depth unavailable; downgrading to terminal capability"
);
}
fn is_stdout_tty() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}
#[cfg(feature = "color-truecolor")]
#[must_use]
pub fn emit_truecolor_fg(r: u8, g: u8, b: u8) -> String {
let mut s = String::with_capacity(20);
s.push_str("\x1b[38;2;");
push_u8(&mut s, r);
s.push(';');
push_u8(&mut s, g);
s.push(';');
push_u8(&mut s, b);
s.push('m');
s
}
#[cfg(feature = "color-truecolor")]
#[must_use]
pub fn emit_truecolor_bg(r: u8, g: u8, b: u8) -> String {
let mut s = String::with_capacity(20);
s.push_str("\x1b[48;2;");
push_u8(&mut s, r);
s.push(';');
push_u8(&mut s, g);
s.push(';');
push_u8(&mut s, b);
s.push('m');
s
}
#[cfg(feature = "color-256")]
#[must_use]
pub fn emit_256_fg(n: u8) -> String {
let mut s = String::with_capacity(12);
s.push_str("\x1b[38;5;");
push_u8(&mut s, n);
s.push('m');
s
}
#[cfg(feature = "color-256")]
#[must_use]
pub fn emit_256_bg(n: u8) -> String {
let mut s = String::with_capacity(12);
s.push_str("\x1b[48;5;");
push_u8(&mut s, n);
s.push('m');
s
}
#[cfg(any(feature = "color-truecolor", feature = "color-256"))]
fn push_u8(s: &mut String, n: u8) {
if n >= 100 {
s.push(((n / 100) + b'0') as char);
s.push((((n / 10) % 10) + b'0') as char);
s.push(((n % 10) + b'0') as char);
} else if n >= 10 {
s.push(((n / 10) + b'0') as char);
s.push(((n % 10) + b'0') as char);
} else {
s.push((n + b'0') as char);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_color16() {
assert_eq!(ColorDepth::default(), ColorDepth::Color16);
}
#[test]
fn resolve_truecolor_to_truecolor_no_warning() {
let r = resolve_depth(ColorDepth::Truecolor, ColorDepth::Truecolor, false);
assert_eq!(r, ColorDepth::Truecolor);
}
#[test]
fn resolve_truecolor_to_color16_downgrades() {
let r = resolve_depth(ColorDepth::Truecolor, ColorDepth::Color16, true);
assert_eq!(r, ColorDepth::Color16);
}
#[test]
fn resolve_truecolor_to_color256_downgrades() {
let r = resolve_depth(ColorDepth::Truecolor, ColorDepth::Color256, true);
assert_eq!(r, ColorDepth::Color256);
}
#[test]
fn resolve_color256_to_truecolor_uses_color256() {
let r = resolve_depth(ColorDepth::Color256, ColorDepth::Truecolor, false);
assert_eq!(r, ColorDepth::Color256);
}
#[test]
fn resolve_color16_always_color16() {
let r = resolve_depth(ColorDepth::Color16, ColorDepth::Truecolor, false);
assert_eq!(r, ColorDepth::Color16);
}
#[cfg(feature = "color-truecolor")]
#[test]
fn truecolor_fg_emits_canonical_sgr() {
let s = emit_truecolor_fg(255, 128, 0);
assert_eq!(s, "\x1b[38;2;255;128;0m");
}
#[cfg(feature = "color-truecolor")]
#[test]
fn truecolor_bg_emits_canonical_sgr() {
let s = emit_truecolor_bg(0, 0, 0);
assert_eq!(s, "\x1b[48;2;0;0;0m");
}
#[cfg(feature = "color-truecolor")]
#[test]
fn truecolor_no_extra_chars() {
let s = emit_truecolor_fg(1, 2, 3);
for ch in s.chars() {
assert!(ch.is_ascii(), "non-ASCII byte in SGR: {ch:?}");
assert!(
ch == '\x1b' || !ch.is_control(),
"control byte other than ESC in SGR: {ch:?}"
);
}
}
#[cfg(feature = "color-256")]
#[test]
fn color256_fg_emits_canonical_sgr() {
let s = emit_256_fg(196);
assert_eq!(s, "\x1b[38;5;196m");
}
#[cfg(feature = "color-256")]
#[test]
fn color256_bg_emits_canonical_sgr() {
let s = emit_256_bg(21);
assert_eq!(s, "\x1b[48;5;21m");
}
#[cfg(feature = "color-256")]
#[test]
fn color256_edge_indices() {
assert_eq!(emit_256_fg(0), "\x1b[38;5;0m");
assert_eq!(emit_256_fg(255), "\x1b[38;5;255m");
}
}