use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Depth {
None,
Ansi16,
Ansi256,
TrueColor,
}
impl Depth {
pub fn detect(is_terminal: bool) -> Depth {
let var = |name| crate::env::var(name);
let text = |name| var(name).and_then(|v| v.into_string().ok());
if var("NO_COLOR").is_some_and(|v| !v.is_empty()) {
return Depth::None;
}
let forced = match text("FORCE_COLOR").as_deref() {
Some("" | "true") => Depth::Ansi16,
Some("false" | "0") => return Depth::None,
Some(level) => match level.parse::<u8>() {
Ok(0) => return Depth::None,
Ok(1) | Err(_) => Depth::Ansi16,
Ok(2) => Depth::Ansi256,
Ok(_) => Depth::TrueColor,
},
None if var("CLICOLOR_FORCE").is_some_and(|v| !v.is_empty() && v != "0") => {
Depth::Ansi16
}
None => Depth::None,
};
let term = text("TERM");
if forced == Depth::None {
let terminal = is_terminal && !crate::env::overridden();
let refused = var("CLICOLOR").is_some_and(|v| v == "0");
let dumb = term.as_deref().is_none_or(|term| term == "dumb");
if !terminal || refused || dumb {
return Depth::None;
}
}
let term = term.as_deref().unwrap_or_default();
let program = text("TERM_PROGRAM");
let detected = if matches!(text("COLORTERM").as_deref(), Some("truecolor" | "24bit"))
|| term.ends_with("direct")
|| term.ends_with("truecolor")
|| program.as_deref() == Some("iTerm.app")
{
Depth::TrueColor
} else if term.ends_with("256")
|| term.ends_with("256color")
|| program.as_deref() == Some("Apple_Terminal")
{
Depth::Ansi256
} else {
Depth::Ansi16
};
detected.max(forced)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(missing_docs, reason = "the names are the colors")]
pub enum Ansi {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
}
impl Ansi {
const ALL: [Ansi; 16] = [
Ansi::Black,
Ansi::Red,
Ansi::Green,
Ansi::Yellow,
Ansi::Blue,
Ansi::Magenta,
Ansi::Cyan,
Ansi::White,
Ansi::BrightBlack,
Ansi::BrightRed,
Ansi::BrightGreen,
Ansi::BrightYellow,
Ansi::BrightBlue,
Ansi::BrightMagenta,
Ansi::BrightCyan,
Ansi::BrightWhite,
];
pub const fn index(self) -> u8 {
self as u8
}
const fn code(self) -> u8 {
let i = self.index();
if i < 8 { 30 + i } else { 90 + i - 8 }
}
const fn rgb(self) -> (u8, u8, u8) {
match self {
Ansi::Black => (0, 0, 0),
Ansi::Red => (205, 0, 0),
Ansi::Green => (0, 205, 0),
Ansi::Yellow => (205, 205, 0),
Ansi::Blue => (0, 0, 238),
Ansi::Magenta => (205, 0, 205),
Ansi::Cyan => (0, 205, 205),
Ansi::White => (229, 229, 229),
Ansi::BrightBlack => (127, 127, 127),
Ansi::BrightRed => (255, 0, 0),
Ansi::BrightGreen => (0, 255, 0),
Ansi::BrightYellow => (255, 255, 0),
Ansi::BrightBlue => (92, 92, 255),
Ansi::BrightMagenta => (255, 0, 255),
Ansi::BrightCyan => (0, 255, 255),
Ansi::BrightWhite => (255, 255, 255),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Color {
Ansi(Ansi),
Ansi256(u8),
Rgb(u8, u8, u8),
}
const CUBE: [u8; 6] = [0, 95, 135, 175, 215, 255];
impl Color {
pub const fn depth(self) -> Depth {
match self {
Color::Ansi(_) => Depth::Ansi16,
Color::Ansi256(_) => Depth::Ansi256,
Color::Rgb(..) => Depth::TrueColor,
}
}
pub fn rgb(self) -> (u8, u8, u8) {
match self {
Color::Ansi(a) => a.rgb(),
Color::Ansi256(n @ 0..=15) => Ansi::ALL[usize::from(n)].rgb(),
Color::Ansi256(n @ 16..=231) => {
let n = n - 16;
let level = |i: u8| CUBE[usize::from(i)];
(level(n / 36), level(n / 6 % 6), level(n % 6))
}
Color::Ansi256(n) => {
let gray = 8 + (n - 232) * 10;
(gray, gray, gray)
}
Color::Rgb(r, g, b) => (r, g, b),
}
}
pub fn at(self, depth: Depth) -> Option<Color> {
match depth {
Depth::None => None,
_ if self.depth() <= depth => Some(self),
Depth::Ansi256 => Some(Color::Ansi256(to_256(self.rgb()))),
Depth::Ansi16 | Depth::TrueColor => Some(Color::Ansi(to_16(self.rgb()))),
}
}
fn write_sgr(self, out: &mut String) {
let _ = match self {
Color::Ansi(a) => write!(out, "{}", a.code()),
Color::Ansi256(n) => write!(out, "38;5;{n}"),
Color::Rgb(r, g, b) => write!(out, "38;2;{r};{g};{b}"),
};
}
}
fn distance((r1, g1, b1): (u8, u8, u8), (r2, g2, b2): (u8, u8, u8)) -> u32 {
let d = |a: u8, b: u8| u32::from(a.abs_diff(b)).pow(2);
2 * d(r1, r2) + 4 * d(g1, g2) + 3 * d(b1, b2)
}
fn to_16(rgb: (u8, u8, u8)) -> Ansi {
let (r, g, b) = (i32::from(rgb.0), i32::from(rgb.1), i32::from(rgb.2));
let (max, min) = (r.max(g).max(b), r.min(g).min(b));
let chroma = max - min;
if chroma < 32 {
return Ansi::ALL
.into_iter()
.min_by_key(|a| distance(a.rgb(), rgb))
.unwrap_or(Ansi::White);
}
let hue = if max == r {
(60 * (g - b) / chroma + 360) % 360
} else if max == g {
120 + 60 * (b - r) / chroma
} else {
240 + 60 * (r - g) / chroma
};
let (normal, bright) = match (hue + 30) / 60 % 6 {
0 => (Ansi::Red, Ansi::BrightRed),
1 => (Ansi::Yellow, Ansi::BrightYellow),
2 => (Ansi::Green, Ansi::BrightGreen),
3 => (Ansi::Cyan, Ansi::BrightCyan),
4 => (Ansi::Blue, Ansi::BrightBlue),
_ => (Ansi::Magenta, Ansi::BrightMagenta),
};
if distance(bright.rgb(), rgb) < distance(normal.rgb(), rgb) {
bright
} else {
normal
}
}
fn to_256(rgb: (u8, u8, u8)) -> u8 {
let level = |c: u8| {
CUBE.iter()
.enumerate()
.min_by_key(|(_, l)| l.abs_diff(c))
.map_or(0, |(i, _)| i as u8)
};
let (r, g, b) = (level(rgb.0), level(rgb.1), level(rgb.2));
let cube = 16 + 36 * r + 6 * g + b;
let average = (u16::from(rgb.0) + u16::from(rgb.1) + u16::from(rgb.2)) / 3;
let step = (average.saturating_sub(3) / 10).min(23) as u8;
let gray = 232 + step;
if distance(Color::Ansi256(gray).rgb(), rgb) < distance(Color::Ansi256(cube).rgb(), rgb) {
gray
} else {
cube
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Paint {
pub fg: Option<Color>,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
}
impl Paint {
pub const NONE: Paint = Paint {
fg: None,
bold: false,
dim: false,
italic: false,
underline: false,
};
pub const fn fg(color: Color) -> Paint {
Paint {
fg: Some(color),
..Paint::NONE
}
}
pub const fn ansi(color: Ansi) -> Paint {
Paint::fg(Color::Ansi(color))
}
pub const fn bold(self) -> Paint {
Paint { bold: true, ..self }
}
pub const fn dim(self) -> Paint {
Paint { dim: true, ..self }
}
pub const fn italic(self) -> Paint {
Paint {
italic: true,
..self
}
}
pub const fn underline(self) -> Paint {
Paint {
underline: true,
..self
}
}
pub const fn depth(self) -> Depth {
match self.fg {
Some(color) => color.depth(),
None => Depth::Ansi16,
}
}
pub fn write(self, out: &mut String, depth: Depth, text: &str) {
if depth == Depth::None || self == Paint::NONE {
out.push_str(text);
return;
}
out.push_str("\u{1b}[");
let mut first = true;
let mut sep = |out: &mut String| {
if !first {
out.push(';');
}
first = false;
};
for (on, code) in [
(self.bold, '1'),
(self.dim, '2'),
(self.italic, '3'),
(self.underline, '4'),
] {
if on {
sep(out);
out.push(code);
}
}
if let Some(color) = self.fg.and_then(|c| c.at(depth)) {
sep(out);
color.write_sgr(out);
}
out.push('m');
out.push_str(text);
out.push_str("\u{1b}[0m");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Palette {
pub header: Paint,
pub program: Paint,
pub flag: Paint,
pub command: Paint,
pub placeholder: Paint,
pub error: Paint,
pub invalid: Paint,
pub valid: Paint,
pub env: Paint,
pub default: Paint,
pub choice: Paint,
pub dim: Paint,
pub code: Paint,
pub emphasis: Paint,
pub strong: Paint,
}
impl Palette {
pub const PLAIN: Palette = Palette {
header: Paint::NONE,
program: Paint::NONE,
flag: Paint::NONE,
command: Paint::NONE,
placeholder: Paint::NONE,
error: Paint::NONE,
invalid: Paint::NONE,
valid: Paint::NONE,
env: Paint::NONE,
default: Paint::NONE,
choice: Paint::NONE,
dim: Paint::NONE,
code: Paint::NONE,
emphasis: Paint::NONE,
strong: Paint::NONE,
};
pub const DEFAULT: Palette = Palette {
header: Paint::ansi(Ansi::Cyan).bold(),
program: Paint::NONE,
flag: Paint::ansi(Ansi::Green).bold(),
command: Paint::ansi(Ansi::Green).bold(),
placeholder: Paint::ansi(Ansi::Cyan),
error: Paint::ansi(Ansi::Red).bold(),
invalid: Paint::ansi(Ansi::Red),
valid: Paint::ansi(Ansi::Green),
env: Paint::ansi(Ansi::Cyan),
default: Paint::ansi(Ansi::Green),
choice: Paint::ansi(Ansi::BrightGreen),
dim: Paint::NONE.dim(),
code: Paint::NONE.bold(),
emphasis: Paint::NONE.italic(),
strong: Paint::NONE.bold(),
};
pub const DEFAULT_256: Palette = Palette {
header: Paint::fg(Color::Ansi256(73)).bold(),
program: Paint::NONE,
flag: Paint::fg(Color::Ansi256(71)).bold(),
command: Paint::fg(Color::Ansi256(71)).bold(),
placeholder: Paint::fg(Color::Ansi256(109)),
error: Paint::fg(Color::Ansi256(167)).bold(),
invalid: Paint::fg(Color::Ansi256(174)),
valid: Paint::fg(Color::Ansi256(108)),
env: Paint::fg(Color::Ansi256(37)),
default: Paint::fg(Color::Ansi256(72)),
choice: Paint::fg(Color::Ansi256(71)),
dim: Paint::fg(Color::Ansi256(245)),
code: Paint::NONE.bold(),
emphasis: Paint::NONE.italic(),
strong: Paint::NONE.bold(),
};
pub const CLAP: Palette = Palette {
header: Paint::NONE.bold().underline(),
program: Paint::NONE.bold(),
flag: Paint::NONE.bold(),
command: Paint::NONE.bold(),
placeholder: Paint::NONE,
error: Paint::ansi(Ansi::Red).bold(),
invalid: Paint::ansi(Ansi::Yellow),
valid: Paint::ansi(Ansi::Green),
env: Paint::NONE,
default: Paint::NONE,
choice: Paint::NONE,
dim: Paint::NONE,
code: Paint::NONE,
emphasis: Paint::NONE,
strong: Paint::NONE,
};
pub const fn depth(&self) -> Depth {
let roles = [
self.header,
self.program,
self.flag,
self.command,
self.placeholder,
self.error,
self.invalid,
self.valid,
self.env,
self.default,
self.choice,
self.dim,
self.code,
self.emphasis,
self.strong,
];
let mut deepest = Depth::Ansi16;
let mut i = 0;
while i < roles.len() {
let depth = roles[i].depth();
if depth as u8 > deepest as u8 {
deepest = depth;
}
i += 1;
}
deepest
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Theme {
pub ansi16: Palette,
pub ansi256: Option<Palette>,
pub truecolor: Option<Palette>,
}
impl Theme {
pub const DEFAULT: Theme = Theme {
ansi16: Palette::DEFAULT,
ansi256: Some(Palette::DEFAULT_256),
truecolor: None,
};
pub const fn uniform(palette: Palette) -> Theme {
Theme {
ansi16: palette,
ansi256: None,
truecolor: None,
}
}
pub fn palette(&self, depth: Depth) -> Palette {
match depth {
Depth::None => Palette::PLAIN,
Depth::Ansi16 => self.ansi16,
Depth::Ansi256 => self.ansi256.unwrap_or(self.ansi16),
Depth::TrueColor => self.truecolor.or(self.ansi256).unwrap_or(self.ansi16),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn colors_map_to_the_nearest_the_terminal_has() {
let orange = Color::Rgb(255, 135, 0);
assert_eq!(orange.at(Depth::TrueColor), Some(orange));
assert_eq!(orange.at(Depth::Ansi256), Some(Color::Ansi256(208)));
assert_eq!(orange.at(Depth::Ansi16), Some(Color::Ansi(Ansi::Yellow)));
assert_eq!(orange.at(Depth::None), None);
assert_eq!(
Color::Rgb(0, 255, 255).at(Depth::Ansi256),
Some(Color::Ansi256(51))
);
assert_eq!(
Color::Rgb(128, 128, 128).at(Depth::Ansi256),
Some(Color::Ansi256(244))
);
assert_eq!(
Color::Ansi256(196).at(Depth::Ansi16),
Some(Color::Ansi(Ansi::BrightRed))
);
let cyan = Color::Ansi(Ansi::Cyan);
assert_eq!(cyan.at(Depth::Ansi256), Some(cyan));
}
#[test]
fn a_paint_is_one_sequence() {
let mut out = String::new();
Paint::ansi(Ansi::Yellow)
.bold()
.write(&mut out, Depth::Ansi16, "Usage:");
assert_eq!(out, "\u{1b}[1;33mUsage:\u{1b}[0m");
out.clear();
Paint::fg(Color::Rgb(1, 2, 3))
.underline()
.write(&mut out, Depth::TrueColor, "x");
assert_eq!(out, "\u{1b}[4;38;2;1;2;3mx\u{1b}[0m");
out.clear();
Paint::ansi(Ansi::BrightCyan).write(&mut out, Depth::Ansi16, "x");
assert_eq!(out, "\u{1b}[96mx\u{1b}[0m");
out.clear();
Paint::ansi(Ansi::Red).write(&mut out, Depth::None, "x");
assert_eq!(out, "x");
}
#[test]
fn a_uniform_theme_maps_its_palette_down() {
let theme = Theme::uniform(Palette::DEFAULT_256);
assert_eq!(theme.palette(Depth::TrueColor), Palette::DEFAULT_256);
assert_eq!(theme.palette(Depth::Ansi256), Palette::DEFAULT_256);
let shown = |paint: Paint| paint.fg.and_then(|c| c.at(Depth::Ansi16));
let palette = theme.palette(Depth::Ansi16);
let ansi = |a| Some(Color::Ansi(a));
assert_eq!(shown(palette.header), ansi(Ansi::Cyan));
assert_eq!(shown(palette.flag), ansi(Ansi::Green));
assert_eq!(shown(palette.placeholder), ansi(Ansi::Cyan));
assert_eq!(shown(palette.error), ansi(Ansi::Red));
assert_eq!(shown(palette.invalid), ansi(Ansi::Red));
assert_eq!(shown(palette.valid), ansi(Ansi::Green));
}
#[test]
fn a_theme_uses_the_richest_palette_the_terminal_shows() {
let rich = Palette {
header: Paint::fg(Color::Rgb(255, 135, 0)),
..Palette::DEFAULT
};
let theme = Theme {
truecolor: Some(rich),
..Theme::DEFAULT
};
assert_eq!(theme.palette(Depth::TrueColor), rich);
assert_eq!(theme.palette(Depth::Ansi256), Palette::DEFAULT_256);
assert_eq!(
Theme::DEFAULT.palette(Depth::TrueColor),
Palette::DEFAULT_256
);
assert_eq!(Theme::DEFAULT.palette(Depth::Ansi16), Palette::DEFAULT);
assert_eq!(theme.palette(Depth::None), Palette::PLAIN);
assert_eq!(rich.depth(), Depth::TrueColor);
assert_eq!(Palette::DEFAULT.depth(), Depth::Ansi16);
assert_eq!(Palette::DEFAULT_256.depth(), Depth::Ansi256);
}
}