supercode_frontend_tui/terminal/
palette.rs1use ratatui::style::Color;
10
11use crate::foundation::color::perceptual_distance;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum ColorLevel {
16 TrueColor,
18 Ansi256,
20 Monochrome,
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub struct ColorCapabilities {
27 pub level: ColorLevel,
28 pub color_enabled: bool,
29}
30
31impl ColorCapabilities {
32 pub fn resolve(
37 no_color: bool,
38 term: Option<&str>,
39 colorterm: Option<&str>,
40 force_color: bool,
41 ) -> Self {
42 if no_color && !force_color {
43 return Self::monochrome();
44 }
45 if term.is_some_and(|value| value.eq_ignore_ascii_case("dumb")) && !force_color {
46 return Self::monochrome();
47 }
48
49 let truecolor = colorterm.is_some_and(|value| {
50 value.eq_ignore_ascii_case("truecolor") || value.eq_ignore_ascii_case("24bit")
51 });
52 let ansi256 = term.is_some_and(|value| value.to_ascii_lowercase().contains("256color"));
53 let level = if truecolor {
54 ColorLevel::TrueColor
55 } else if ansi256 || force_color {
56 ColorLevel::Ansi256
57 } else {
58 ColorLevel::Monochrome
59 };
60 Self {
61 level,
62 color_enabled: level != ColorLevel::Monochrome,
63 }
64 }
65
66 pub const fn monochrome() -> Self {
67 Self {
68 level: ColorLevel::Monochrome,
69 color_enabled: false,
70 }
71 }
72
73 pub fn best_color(self, target: (u8, u8, u8)) -> Color {
75 match self.level {
76 ColorLevel::TrueColor => Color::Rgb(target.0, target.1, target.2),
77 ColorLevel::Ansi256 => Color::Indexed(nearest_xterm_index(target)),
78 ColorLevel::Monochrome => Color::Reset,
79 }
80 }
81}
82
83fn nearest_xterm_index(target: (u8, u8, u8)) -> u8 {
84 xterm_fixed_colors()
85 .min_by(|(_, left), (_, right)| {
86 perceptual_distance(*left, target)
87 .partial_cmp(&perceptual_distance(*right, target))
88 .unwrap_or(std::cmp::Ordering::Equal)
89 })
90 .map_or(16, |(index, _)| index)
91}
92
93fn xterm_fixed_colors() -> impl Iterator<Item = (u8, (u8, u8, u8))> {
94 let cube = (0u8..6).flat_map(|r| {
95 (0u8..6).flat_map(move |g| {
96 (0u8..6).map(move |b| {
97 let index = 16 + 36 * r + 6 * g + b;
98 (
99 index,
100 (xterm_component(r), xterm_component(g), xterm_component(b)),
101 )
102 })
103 })
104 });
105 let grayscale = (0u8..24).map(|offset| {
106 let value = 8 + offset * 10;
107 (232 + offset, (value, value, value))
108 });
109 cube.chain(grayscale)
110}
111
112const fn xterm_component(value: u8) -> u8 {
113 if value == 0 {
114 0
115 } else {
116 55 + value * 40
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn truecolor_is_not_quantized() {
126 let capabilities =
127 ColorCapabilities::resolve(false, Some("xterm-256color"), Some("truecolor"), false);
128 assert_eq!(capabilities.level, ColorLevel::TrueColor);
129 assert_eq!(
130 capabilities.best_color((12, 34, 56)),
131 Color::Rgb(12, 34, 56)
132 );
133 }
134
135 #[test]
136 fn ansi256_uses_a_stable_index() {
137 let capabilities = ColorCapabilities::resolve(false, Some("xterm-256color"), None, false);
138 assert_eq!(capabilities.level, ColorLevel::Ansi256);
139 assert_eq!(capabilities.best_color((255, 0, 0)), Color::Indexed(196));
140 assert_eq!(
141 capabilities.best_color((128, 128, 128)),
142 Color::Indexed(244)
143 );
144 }
145
146 #[test]
147 fn no_color_disables_authored_colors() {
148 let capabilities =
149 ColorCapabilities::resolve(true, Some("xterm-256color"), Some("truecolor"), false);
150 assert_eq!(capabilities, ColorCapabilities::monochrome());
151 assert_eq!(capabilities.best_color((255, 0, 0)), Color::Reset);
152 }
153
154 #[test]
155 fn dumb_terminal_is_monochrome_unless_forced() {
156 assert_eq!(
157 ColorCapabilities::resolve(false, Some("dumb"), None, false),
158 ColorCapabilities::monochrome()
159 );
160 assert_eq!(
161 ColorCapabilities::resolve(false, Some("dumb"), None, true).level,
162 ColorLevel::Ansi256
163 );
164 }
165}