use std::io::IsTerminal;
use crate::token::{Token, TokenKind};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Depth {
Off,
Ansi16,
Ansi256,
TrueColor,
}
impl Depth {
#[must_use]
pub fn detect() -> Depth {
let is_terminal = std::io::stdout().is_terminal();
let depth = Depth::of(is_terminal, &|name| std::env::var(name).ok());
if depth != Depth::Off && cfg!(windows) && !enable_vt() {
return Depth::Off;
}
depth
}
pub fn of(is_terminal: bool, env: &dyn Fn(&str) -> Option<String>) -> Depth {
let set = |name: &str| env(name).is_some_and(|v| !v.is_empty());
if set("NO_COLOR") || !is_terminal {
return Depth::Off;
}
let term = env("TERM").unwrap_or_default();
if term == "dumb" {
return Depth::Off;
}
let colorterm = env("COLORTERM").unwrap_or_default();
if colorterm == "truecolor"
|| colorterm == "24bit"
|| set("WT_SESSION")
|| env("TERM_PROGRAM").is_some_and(|p| p == "vscode")
|| term.starts_with("xterm-kitty")
|| term.starts_with("alacritty")
{
return Depth::TrueColor;
}
if term.contains("256color") {
return Depth::Ansi256;
}
if cfg!(windows) && term.is_empty() {
return Depth::TrueColor;
}
Depth::Ansi16
}
#[must_use]
pub fn forced() -> Depth {
console_ready();
Depth::of(true, &|name| std::env::var(name).ok()).max(Depth::Ansi16)
}
#[must_use]
pub fn parse(s: &str) -> Option<Depth> {
match s {
"16" | "ansi" => Some(Depth::Ansi16),
"256" | "ansi256" => Some(Depth::Ansi256),
"truecolor" | "24bit" | "16m" => Some(Depth::TrueColor),
_ => None,
}
}
}
pub fn console_ready() -> bool {
enable_vt()
}
#[cfg(windows)]
fn enable_vt() -> bool {
match winapi_util::console::Console::stdout() {
Ok(mut console) => match console.set_virtual_terminal_processing(true) {
Ok(()) => true,
Err(_refused) => false,
},
Err(_not_a_console) => false,
}
}
#[cfg(not(windows))]
fn enable_vt() -> bool {
true
}
const BASE_RGB: [(u8, u8, u8); 16] = [
(0, 0, 0),
(205, 0, 0),
(0, 205, 0),
(205, 205, 0),
(0, 0, 238),
(205, 0, 205),
(0, 205, 205),
(229, 229, 229),
(127, 127, 127),
(255, 0, 0),
(0, 255, 0),
(255, 255, 0),
(92, 92, 255),
(255, 0, 255),
(0, 255, 255),
(255, 255, 255),
];
const CUBE: [u8; 6] = [0, 95, 135, 175, 215, 255];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Color {
Base(u8),
Index(u8),
Rgb(u8, u8, u8),
}
const NAMES: [&str; 8] = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"];
impl Color {
pub fn parse(s: &str) -> Result<Color, String> {
if let Some(i) = NAMES.iter().position(|n| *n == s) {
return Ok(Color::Base(u8::try_from(i).unwrap_or(7)));
}
if let Some(hex) = s.strip_prefix('#') {
if hex.len() != 6 {
return Err(format!("{s:?} is not a #rrggbb color"));
}
let mut channels = [0u8; 3];
for (slot, at) in channels.iter_mut().zip([0, 2, 4]) {
*slot = u8::from_str_radix(&hex[at..at + 2], 16)
.map_err(|e| format!("{s:?} is not a #rrggbb color: {e}"))?;
}
return Ok(Color::Rgb(channels[0], channels[1], channels[2]));
}
if s.starts_with("0x") {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() == 3 {
let mut channels = [0u8; 3];
for (slot, part) in channels.iter_mut().zip(&parts) {
let digits = part.strip_prefix("0x").ok_or_else(|| {
format!("{s:?} is not a 0xRR,0xGG,0xBB color")
})?;
*slot = u8::from_str_radix(digits, 16)
.map_err(|e| format!("{s:?} is not a 0xRR,0xGG,0xBB color: {e}"))?;
}
return Ok(Color::Rgb(channels[0], channels[1], channels[2]));
}
return Err(format!("{s:?} is not a 0xRR,0xGG,0xBB color"));
}
if s.bytes().all(|b| b.is_ascii_digit()) && !s.is_empty() {
return match s.parse::<u8>() {
Ok(i) => Ok(Color::Index(i)),
Err(e) => Err(format!("{s:?} is not an entry of the 256-color table: {e}")),
};
}
Err(format!(
"{s:?} is not a color; write black, red, green, yellow, blue, magenta, cyan or white, #rrggbb, 0xRR,0xGG,0xBB, or a number 0-255"
))
}
#[must_use]
pub fn rgb(self) -> (u8, u8, u8) {
match self {
Color::Rgb(r, g, b) => (r, g, b),
Color::Base(i) => BASE_RGB[usize::from(i) % 16],
Color::Index(i) if i < 16 => BASE_RGB[usize::from(i)],
Color::Index(i) if i < 232 => {
let i = usize::from(i) - 16;
(CUBE[i / 36], CUBE[(i / 6) % 6], CUBE[i % 6])
}
Color::Index(i) => {
let gray = 8 + 10 * (i - 232);
(gray, gray, gray)
}
}
}
#[must_use]
pub fn at(self, depth: Depth) -> Option<Color> {
match (depth, self) {
(Depth::Off, _) => None,
(_, Color::Base(_)) | (Depth::TrueColor, _) | (Depth::Ansi256, Color::Index(_)) => Some(self),
(Depth::Ansi256, Color::Rgb(..)) => Some(Color::Index(nearest_index(self.rgb()))),
(Depth::Ansi16, _) => Some(Color::Base(nearest_base(self.rgb()))),
}
}
fn sgr(self, foreground: bool) -> String {
match self {
Color::Base(i) => {
let (dim, bright) = if foreground { (30, 90) } else { (40, 100) };
let i = u16::from(i % 16);
if i < 8 { (dim + i).to_string() } else { (bright + i - 8).to_string() }
}
Color::Index(i) => format!("{};5;{i}", if foreground { 38 } else { 48 }),
Color::Rgb(r, g, b) => format!("{};2;{r};{g};{b}", if foreground { 38 } else { 48 }),
}
}
}
fn distance(a: (u8, u8, u8), b: (u8, u8, u8)) -> u32 {
let d = |x: u8, y: u8| {
let d = i32::from(x) - i32::from(y);
(d * d).unsigned_abs()
};
d(a.0, b.0) + d(a.1, b.1) + d(a.2, b.2)
}
fn nearest_index(rgb: (u8, u8, u8)) -> u8 {
(16..=255u8).min_by_key(|&i| distance(Color::Index(i).rgb(), rgb)).unwrap_or(16)
}
fn nearest_base(rgb: (u8, u8, u8)) -> u8 {
(0..16u8).min_by_key(|&i| distance(BASE_RGB[usize::from(i)], rgb)).unwrap_or(0)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub intense: bool,
pub underline: bool,
}
impl Style {
#[must_use]
pub fn sgr(&self, depth: Depth) -> String {
if depth == Depth::Off {
return String::new();
}
let mut parts: Vec<String> = Vec::new();
if self.bold {
parts.push("1".to_string());
}
if self.underline {
parts.push("4".to_string());
}
if let Some(fg) = self.fg.and_then(|c| c.at(depth)) {
let fg = match fg {
Color::Base(i) if self.intense && i < 8 => Color::Base(i + 8),
other => other,
};
parts.push(fg.sgr(true));
}
if let Some(bg) = self.bg.and_then(|c| c.at(depth)) {
parts.push(bg.sgr(false));
}
parts.join(";")
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum KindPaint {
None,
#[default]
Values,
All,
}
impl KindPaint {
#[must_use]
pub fn parse(s: &str) -> Option<KindPaint> {
match s {
"none" | "off" => Some(KindPaint::None),
"values" => Some(KindPaint::Values),
"all" => Some(KindPaint::All),
_ => None,
}
}
#[must_use]
pub fn paints(self, kind: TokenKind) -> bool {
match self {
KindPaint::None => false,
KindPaint::All => true,
KindPaint::Values => !matches!(
kind,
TokenKind::Word
| TokenKind::Punct
| TokenKind::Open(_)
| TokenKind::Close(_)
| TokenKind::Whitespace
| TokenKind::Other
),
}
}
}
const KIND_COLORS: [(TokenKind, Color); 25] = [
(TokenKind::Ip, Color::Rgb(0x00, 0x5f, 0xff)),
(TokenKind::Cidr, Color::Rgb(0x00, 0x00, 0xd7)),
(TokenKind::Mac, Color::Rgb(0x5f, 0x87, 0xff)),
(TokenKind::Url, Color::Rgb(0x00, 0x5f, 0xd7)),
(TokenKind::Email, Color::Rgb(0x5f, 0x5f, 0xff)),
(TokenKind::Phone, Color::Rgb(0x00, 0x00, 0xaf)),
(TokenKind::Path, Color::Rgb(0x00, 0xd7, 0xd7)),
(TokenKind::Geo, Color::Rgb(0x00, 0xaf, 0xaf)),
(TokenKind::Number, Color::Rgb(0xff, 0xd7, 0x00)),
(TokenKind::Percent, Color::Rgb(0xff, 0xaf, 0x00)),
(TokenKind::ByteSize, Color::Rgb(0xd7, 0xaf, 0x00)),
(TokenKind::Money, Color::Rgb(0xff, 0xff, 0x5f)),
(TokenKind::Duration, Color::Rgb(0xd7, 0x87, 0x00)),
(TokenKind::Quantity, Color::Rgb(0xaf, 0x87, 0x00)),
(TokenKind::Timestamp, Color::Rgb(0x00, 0xd7, 0x5f)),
(TokenKind::Uuid, Color::Rgb(0xd7, 0x5f, 0xd7)),
(TokenKind::Version, Color::Rgb(0xff, 0x5f, 0xff)),
(TokenKind::HexColor, Color::Rgb(0xaf, 0x00, 0xaf)),
(TokenKind::Quoted, Color::Rgb(0xd7, 0x00, 0xd7)),
(TokenKind::CreditCard, Color::Rgb(0xff, 0x00, 0x00)),
(TokenKind::Jwt, Color::Rgb(0xd7, 0x00, 0x00)),
(TokenKind::Base64, Color::Rgb(0xff, 0x00, 0x5f)),
(TokenKind::HashDigest, Color::Rgb(0x87, 0x00, 0x00)),
(TokenKind::Hex, Color::Rgb(0xaf, 0x00, 0x00)),
(TokenKind::Word, Color::Rgb(0xaf, 0xaf, 0xaf)),
];
const ALARM_KINDS: [TokenKind; 5] =
[TokenKind::CreditCard, TokenKind::Jwt, TokenKind::Base64, TokenKind::HashDigest, TokenKind::Hex];
#[must_use]
pub fn kind_style(kind: TokenKind) -> Style {
let fg = KIND_COLORS.iter().find(|(k, _)| *k == kind).map(|&(_, c)| c);
Style { fg, underline: ALARM_KINDS.contains(&kind), ..Style::default() }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Role {
Match,
Path,
Line,
Column,
Separator,
}
impl Role {
pub const ALL: [Role; 5] = [Role::Match, Role::Path, Role::Line, Role::Column, Role::Separator];
#[must_use]
pub fn parse(s: &str) -> Option<Role> {
match s {
"match" => Some(Role::Match),
"path" => Some(Role::Path),
"line" => Some(Role::Line),
"column" => Some(Role::Column),
"separator" => Some(Role::Separator),
_ => None,
}
}
fn slot(self) -> usize {
match self {
Role::Match => 0,
Role::Path => 1,
Role::Line => 2,
Role::Column => 3,
Role::Separator => 4,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Palette {
styles: [Style; 5],
kinds: KindPaint,
kind_overrides: Vec<(TokenKind, Style)>,
capture_overrides: Vec<(String, Style)>,
}
impl Default for Palette {
fn default() -> Self {
Palette::base()
}
}
impl Palette {
#[must_use]
pub fn base() -> Palette {
let fg = |i: u8| Style { fg: Some(Color::Base(i)), ..Style::default() };
Palette {
styles: [
Style { fg: Some(Color::Base(1)), bold: true, ..Style::default() },
fg(5),
fg(2),
fg(2),
fg(6),
],
kinds: KindPaint::default(),
kind_overrides: Vec::new(),
capture_overrides: Vec::new(),
}
}
#[must_use]
pub fn plain() -> Palette {
Palette {
styles: [Style::default(); 5],
kinds: KindPaint::None,
kind_overrides: Vec::new(),
capture_overrides: Vec::new(),
}
}
#[must_use]
pub fn style(&self, role: Role) -> Style {
self.styles[role.slot()]
}
#[must_use]
pub fn kind_paint(&self) -> KindPaint {
self.kinds
}
pub fn set_kind_paint(&mut self, level: KindPaint) {
self.kinds = level;
}
#[must_use]
pub fn kind(&self, kind: TokenKind) -> Option<Style> {
if let Some((_, style)) = self.kind_overrides.iter().rev().find(|(k, _)| *k == kind) {
return Some(*style);
}
if !self.kinds.paints(kind) {
return None;
}
let style = kind_style(kind);
(style != Style::default()).then_some(style)
}
#[must_use]
pub fn capture(&self, name: &str) -> Option<Style> {
self.capture_overrides.iter().rev().find(|(n, _)| n == name).map(|(_, style)| *style)
}
#[must_use]
pub fn paints_captures(&self) -> bool {
!self.capture_overrides.is_empty()
}
pub fn set(&mut self, spec: &str) -> Result<(), String> {
if let Some(rest) = spec.strip_prefix("kind:") {
return self.set_kind(spec, rest);
}
if let Some(rest) = spec.strip_prefix("capture:") {
let (name, rest) = rest.split_once(':').ok_or_else(|| {
format!("{spec:?} names a register and nothing to paint it; write capture:NAME:fg:COLOR")
})?;
if name.is_empty() {
return Err(format!("{spec:?} names no register; write capture:NAME:fg:COLOR"));
}
#[expect(
clippy::manual_unwrap_or_default,
reason = "an Option default, which the repository's swallowed-error guard reads as a Result default"
)]
let mut style = match self.capture(name) {
Some(already) => already,
None => Style::default(),
};
apply(spec, rest, &mut style)?;
self.capture_overrides.push((name.to_string(), style));
return Ok(());
}
let mut parts = spec.splitn(3, ':');
let role = parts.next().unwrap_or("");
let role = Role::parse(role).ok_or_else(|| {
format!(
"{spec:?} names no role; a spec opens with match, path, line, column, separator, kind:NAME or capture:NAME"
)
})?;
let rest = match (parts.next(), parts.next()) {
(Some(head), Some(tail)) => format!("{head}:{tail}"),
(Some(head), None) => head.to_string(),
(None, _) => String::new(),
};
apply(spec, &rest, &mut self.styles[role.slot()])
}
fn set_kind(&mut self, spec: &str, rest: &str) -> Result<(), String> {
let (name, rest) = rest.split_once(':').ok_or_else(|| {
format!("{spec:?} names a kind and nothing to paint it; write kind:NAME:fg:COLOR or kind:*:LEVEL")
})?;
if name == "*" {
let level = KindPaint::parse(rest).ok_or_else(|| {
format!("{rest:?} is not a level; kind:* takes none, values or all")
})?;
self.kinds = level;
self.kind_overrides.clear();
return Ok(());
}
let kind = TokenKind::named(name).ok_or_else(|| {
format!("{name:?} is no token kind; kind:* speaks for all of them")
})?;
#[expect(
clippy::manual_unwrap_or_default,
reason = "an Option default, which the repository's swallowed-error guard reads as a Result default"
)]
let mut style = match self.kind(kind) {
Some(already) => already,
None => Style::default(),
};
apply(spec, rest, &mut style)?;
self.kind_overrides.push((kind, style));
Ok(())
}
}
fn apply(spec: &str, rest: &str, style: &mut Style) -> Result<(), String> {
let (head, tail) = match rest.split_once(':') {
Some((head, tail)) => (head, Some(tail)),
None => (rest, None),
};
match (head, tail) {
("none", None) => *style = Style::default(),
("fg", Some(color)) => style.fg = Some(Color::parse(color)?),
("bg", Some(color)) => style.bg = Some(Color::parse(color)?),
("style", Some(which)) => match which {
"bold" => style.bold = true,
"nobold" => style.bold = false,
"intense" => style.intense = true,
"nointense" => style.intense = false,
"underline" => style.underline = true,
"nounderline" => style.underline = false,
other => {
return Err(format!(
"{other:?} is not a style; write bold, nobold, intense, nointense, underline or nounderline"
));
}
},
_ => {
return Err(format!(
"{spec:?} is not a color spec; write NAME:fg:COLOR, NAME:bg:COLOR, NAME:style:STYLE or NAME:none"
));
}
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Painter {
depth: Depth,
palette: Palette,
}
impl Painter {
#[must_use]
pub fn new(depth: Depth, palette: Palette) -> Painter {
Painter { depth, palette }
}
#[must_use]
pub fn off() -> Painter {
Painter { depth: Depth::Off, palette: Palette::plain() }
}
#[must_use]
pub fn depth(&self) -> Depth {
self.depth
}
#[must_use]
pub fn is_on(&self) -> bool {
self.depth != Depth::Off
}
#[must_use]
pub fn palette(&self) -> &Palette {
&self.palette
}
#[must_use]
pub fn paints_kinds(&self) -> bool {
self.is_on()
&& (self.palette.kind_paint() != KindPaint::None
|| !self.palette.kind_overrides.is_empty()
|| self.palette.paints_captures())
}
#[must_use]
pub fn paint_kinds(&self, input: &[u8], span: std::ops::Range<usize>, tokens: &[Token]) -> String {
let plain = || String::from_utf8_lossy(&input[span.clone()]).into_owned();
if !self.paints_kinds() {
return plain();
}
let first = tokens.partition_point(|t| t.end() <= span.start);
let mut out = String::with_capacity(span.len());
let mut at = span.start;
for token in &tokens[first..] {
if token.start() >= span.end {
break;
}
let Some(style) = self.palette.kind(token.kind) else { continue };
let sgr = style.sgr(self.depth);
if sgr.is_empty() {
continue;
}
let from = token.start().max(span.start);
let to = token.end().min(span.end);
if from < at {
continue;
}
out.push_str(&String::from_utf8_lossy(&input[at..from]));
out.push_str("\x1b[");
out.push_str(&sgr);
out.push('m');
out.push_str(&String::from_utf8_lossy(&input[from..to]));
out.push_str("\x1b[0m");
at = to;
}
out.push_str(&String::from_utf8_lossy(&input[at..span.end]));
out
}
#[must_use]
pub fn paint(&self, role: Role, text: &str) -> String {
let sgr = self.palette.style(role).sgr(self.depth);
if sgr.is_empty() {
return text.to_string();
}
format!("\x1b[{sgr}m{text}\x1b[0m")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn env_of(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
let pairs: Vec<(String, String)> =
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
move |name: &str| pairs.iter().find(|(k, _)| k == name).map(|(_, v)| v.clone())
}
fn collapses_to(kind: TokenKind) -> &'static str {
let fg = kind_style(kind).fg.expect("every listed kind carries a color");
match fg.at(Depth::Ansi16).expect("sixteen colors is not off") {
Color::Base(8) => "gray",
Color::Base(i) => NAMES[usize::from(i) % 8],
other => panic!("sixteen colors gave {other:?}, which is not one of them"),
}
}
#[test]
fn every_kind_collapses_onto_its_own_family_at_sixteen_colors() {
let families: [(&str, &[TokenKind]); 5] = [
("blue", &[TokenKind::Ip, TokenKind::Cidr, TokenKind::Mac, TokenKind::Url, TokenKind::Email, TokenKind::Phone]),
("cyan", &[TokenKind::Path, TokenKind::Geo]),
("yellow", &[TokenKind::Number, TokenKind::Percent, TokenKind::ByteSize, TokenKind::Money, TokenKind::Duration, TokenKind::Quantity]),
("green", &[TokenKind::Timestamp]),
("magenta", &[TokenKind::Uuid, TokenKind::Version, TokenKind::HexColor, TokenKind::Quoted]),
];
for (base, kinds) in families {
for kind in kinds {
assert_eq!(collapses_to(*kind), base, "{} left its family", kind.name());
}
}
for kind in ALARM_KINDS {
assert_eq!(collapses_to(kind), "red", "{} left the alarm family", kind.name());
}
}
#[test]
fn the_kinds_stay_apart_where_the_console_can_tell_them_apart() {
let mut at_256: Vec<Color> = KIND_COLORS
.iter()
.map(|(_, c)| c.at(Depth::Ansi256).expect("256 colors is not off"))
.collect();
at_256.sort_by_key(|c| c.rgb());
at_256.dedup();
assert_eq!(at_256.len(), KIND_COLORS.len(), "two kinds share an entry at 256 colors");
for (kind, color) in KIND_COLORS {
assert_eq!(color.at(Depth::TrueColor), Some(color), "{} moved at 24 bits", kind.name());
let at_256 = color.at(Depth::Ansi256).expect("256 colors is not off");
assert_eq!(at_256.rgb(), color.rgb(), "{} is off the 256-color cube", kind.name());
}
}
#[test]
fn the_alarm_kinds_carry_an_underline_at_every_depth() {
for kind in ALARM_KINDS {
let style = kind_style(kind);
assert!(style.underline, "{} carries no underline", kind.name());
for depth in [Depth::Ansi16, Depth::Ansi256, Depth::TrueColor] {
assert!(style.sgr(depth).split(';').any(|p| p == "4"), "{} at {depth:?}", kind.name());
}
}
assert!(!kind_style(TokenKind::Number).underline);
}
#[test]
fn the_levels_decide_which_kinds_are_painted() {
assert!(!KindPaint::None.paints(TokenKind::Ip));
assert!(KindPaint::Values.paints(TokenKind::Ip));
assert!(KindPaint::Values.paints(TokenKind::Quoted), "a quoted string is a value");
assert!(!KindPaint::Values.paints(TokenKind::Word), "words are the quiet part of a line");
assert!(!KindPaint::Values.paints(TokenKind::Punct));
assert!(KindPaint::All.paints(TokenKind::Word));
assert!(KindPaint::All.paints(TokenKind::Punct));
assert_eq!(KindPaint::parse("none"), Some(KindPaint::None));
assert_eq!(KindPaint::parse("values"), Some(KindPaint::Values));
assert_eq!(KindPaint::parse("all"), Some(KindPaint::All));
assert_eq!(KindPaint::parse("sometimes"), None);
assert_eq!(KindPaint::default(), KindPaint::Values);
}
#[test]
fn the_depth_follows_the_environment_and_the_terminal() {
assert_eq!(Depth::of(false, &env_of(&[("COLORTERM", "truecolor")])), Depth::Off);
assert_eq!(Depth::of(true, &env_of(&[("NO_COLOR", "1"), ("COLORTERM", "truecolor")])), Depth::Off);
assert_eq!(Depth::of(true, &env_of(&[("TERM", "dumb")])), Depth::Off);
assert_eq!(Depth::of(true, &env_of(&[("TERM", "xterm"), ("COLORTERM", "24bit")])), Depth::TrueColor);
assert_eq!(Depth::of(true, &env_of(&[("TERM", "xterm-256color"), ("WT_SESSION", "x")])), Depth::TrueColor);
assert_eq!(Depth::of(true, &env_of(&[("TERM", "xterm-256color")])), Depth::Ansi256);
assert_eq!(Depth::of(true, &env_of(&[("TERM", "xterm-kitty")])), Depth::TrueColor);
assert_eq!(Depth::of(true, &env_of(&[("TERM", "xterm")])), Depth::Ansi16);
assert_eq!(Depth::parse("256"), Some(Depth::Ansi256));
assert_eq!(Depth::parse("24bit"), Some(Depth::TrueColor));
assert_eq!(Depth::parse("lots"), None);
}
#[test]
fn a_color_is_sent_at_the_depth_or_at_the_nearest_the_depth_has() {
assert_eq!(Color::parse("red").expect("a name"), Color::Base(1));
assert_eq!(Color::parse("#ff6b6b").expect("hex"), Color::Rgb(0xff, 0x6b, 0x6b));
assert_eq!(Color::parse("0x00,0xff,0x10").expect("rg's form"), Color::Rgb(0, 255, 16));
assert_eq!(Color::parse("208").expect("a table entry"), Color::Index(208));
assert!(Color::parse("#12").expect_err("short hex").contains("#rrggbb"));
assert!(Color::parse("pinkish").expect_err("a word").contains("is not a color"));
assert_eq!(Color::Index(208).rgb(), (255, 135, 0));
assert_eq!(Color::Index(244).rgb(), (128, 128, 128));
assert_eq!(Color::Rgb(255, 0, 0).at(Depth::Ansi16), Some(Color::Base(9)));
assert_eq!(Color::Rgb(200, 0, 0).at(Depth::Ansi16), Some(Color::Base(1)));
assert_eq!(Color::Rgb(255, 135, 0).at(Depth::Ansi256), Some(Color::Index(208)));
assert_eq!(Color::Rgb(255, 135, 0).at(Depth::TrueColor), Some(Color::Rgb(255, 135, 0)));
assert_eq!(Color::Base(5).at(Depth::TrueColor), Some(Color::Base(5)));
assert_eq!(Color::Index(208).at(Depth::Ansi16), Some(Color::Base(3)));
assert_eq!(Color::Base(5).at(Depth::Off), None);
}
#[test]
fn the_palette_takes_overrides_as_ripgrep_spells_them() {
let mut p = Palette::base();
assert_eq!(p.style(Role::Match), Style { fg: Some(Color::Base(1)), bold: true, ..Style::default() });
p.set("match:fg:#ff6b6b").expect("fg");
p.set("match:style:nobold").expect("style");
p.set("path:bg:blue").expect("bg");
p.set("line:style:underline").expect("underline");
p.set("separator:none").expect("none");
assert_eq!(p.style(Role::Match), Style { fg: Some(Color::Rgb(0xff, 0x6b, 0x6b)), ..Style::default() });
assert_eq!(p.style(Role::Path).bg, Some(Color::Base(4)));
assert!(p.style(Role::Line).underline);
assert_eq!(p.style(Role::Separator), Style::default());
assert!(p.set("tone:fg:red").expect_err("no role").contains("names no role"));
assert!(p.set("match:weight:bold").expect_err("no attribute").contains("is not a color spec"));
assert!(p.set("match:style:loud").expect_err("no style").contains("is not a style"));
}
#[test]
fn a_painter_wraps_text_in_the_codes_its_depth_renders() {
let base = Painter::new(Depth::Ansi16, Palette::base());
assert_eq!(base.paint(Role::Match, "x"), "\x1b[1;31mx\x1b[0m");
assert_eq!(base.paint(Role::Path, "p"), "\x1b[35mp\x1b[0m");
assert_eq!(base.paint(Role::Separator, ":"), "\x1b[36m:\x1b[0m");
let mut palette = Palette::base();
palette.set("match:fg:#ff8700").expect("fg");
palette.set("match:style:intense").expect("intense");
palette.set("path:style:intense").expect("intense");
assert_eq!(Painter::new(Depth::TrueColor, palette.clone()).paint(Role::Match, "x"), "\x1b[1;38;2;255;135;0mx\x1b[0m");
assert_eq!(Painter::new(Depth::Ansi256, palette.clone()).paint(Role::Match, "x"), "\x1b[1;38;5;208mx\x1b[0m");
assert_eq!(Painter::new(Depth::Ansi16, palette.clone()).paint(Role::Match, "x"), "\x1b[1;93mx\x1b[0m");
assert_eq!(Painter::new(Depth::Ansi16, palette).paint(Role::Path, "p"), "\x1b[95mp\x1b[0m");
assert_eq!(Painter::off().paint(Role::Match, "x"), "x");
assert!(!Painter::off().is_on());
}
}