1use anstyle::AnsiColor;
19
20#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
23pub enum ColorLevel {
24 Truecolor,
26 Ansi256,
28 Ansi16,
30 None,
32}
33
34impl ColorLevel {
35 pub const ALL: [ColorLevel; 4] = [
37 ColorLevel::Truecolor,
38 ColorLevel::Ansi256,
39 ColorLevel::Ansi16,
40 ColorLevel::None,
41 ];
42
43 pub const fn is_colored(self) -> bool {
45 !matches!(self, ColorLevel::None)
46 }
47}
48
49#[derive(Clone, Copy, PartialEq, Eq, Debug)]
54pub enum Background {
55 Known(Rgb),
56 Assumed,
57}
58
59#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
62pub struct Rgb {
63 pub r: u8,
64 pub g: u8,
65 pub b: u8,
66}
67
68impl Rgb {
69 #[must_use]
70 pub const fn new(r: u8, g: u8, b: u8) -> Self {
71 Self { r, g, b }
72 }
73
74 #[must_use]
76 pub fn from_color(c: irodori::Color) -> Self {
77 Self::new(c.r, c.g, c.b)
78 }
79
80 fn linear(self) -> [f32; 3] {
81 irodori::Color::new(self.r, self.g, self.b).to_linear()
84 }
85
86 #[must_use]
89 pub fn to_ansi256(self) -> u8 {
90 const LEVELS: [u8; 6] = [0, 95, 135, 175, 215, 255];
91 let nearest_level = |c: u8| -> (usize, u8) {
92 let mut best = 0usize;
93 let mut best_d = u16::MAX;
94 for (i, &lv) in LEVELS.iter().enumerate() {
95 let d = (i16::from(c) - i16::from(lv)).unsigned_abs();
96 if d < best_d {
97 best_d = d;
98 best = i;
99 }
100 }
101 (best, LEVELS[best])
102 };
103 let (ri, _) = nearest_level(self.r);
104 let (gi, _) = nearest_level(self.g);
105 let (bi, _) = nearest_level(self.b);
106 let cube_idx = 16 + 36 * ri + 6 * gi + bi;
107 let cube_rgb = Rgb::new(LEVELS[ri], LEVELS[gi], LEVELS[bi]);
108
109 let grey_avg = ((u16::from(self.r) + u16::from(self.g) + u16::from(self.b)) / 3) as u8;
111 let gi2 = ((i16::from(grey_avg) - 8).clamp(0, 238) as f32 / 10.0).round() as i16;
112 let gi2 = gi2.clamp(0, 23) as u8;
113 let grey_val = 8 + gi2 * 10;
114 let grey_idx = 232 + gi2;
115 let grey_rgb = Rgb::new(grey_val, grey_val, grey_val);
116
117 if linear_dist(self, cube_rgb) <= linear_dist(self, grey_rgb) {
119 cube_idx as u8
120 } else {
121 grey_idx
122 }
123 }
124
125 #[must_use]
129 pub fn to_ansi16(self) -> AnsiColor {
130 const REF: [(Rgb, AnsiColor); 16] = [
132 (Rgb::new(0x00, 0x00, 0x00), AnsiColor::Black),
133 (Rgb::new(0x80, 0x00, 0x00), AnsiColor::Red),
134 (Rgb::new(0x00, 0x80, 0x00), AnsiColor::Green),
135 (Rgb::new(0x80, 0x80, 0x00), AnsiColor::Yellow),
136 (Rgb::new(0x00, 0x00, 0x80), AnsiColor::Blue),
137 (Rgb::new(0x80, 0x00, 0x80), AnsiColor::Magenta),
138 (Rgb::new(0x00, 0x80, 0x80), AnsiColor::Cyan),
139 (Rgb::new(0xc0, 0xc0, 0xc0), AnsiColor::White),
140 (Rgb::new(0x80, 0x80, 0x80), AnsiColor::BrightBlack),
141 (Rgb::new(0xff, 0x00, 0x00), AnsiColor::BrightRed),
142 (Rgb::new(0x00, 0xff, 0x00), AnsiColor::BrightGreen),
143 (Rgb::new(0xff, 0xff, 0x00), AnsiColor::BrightYellow),
144 (Rgb::new(0x00, 0x00, 0xff), AnsiColor::BrightBlue),
145 (Rgb::new(0xff, 0x00, 0xff), AnsiColor::BrightMagenta),
146 (Rgb::new(0x00, 0xff, 0xff), AnsiColor::BrightCyan),
147 (Rgb::new(0xff, 0xff, 0xff), AnsiColor::BrightWhite),
148 ];
149 let mut best = AnsiColor::White;
150 let mut best_d = f32::MAX;
151 for (rgb, ansi) in REF {
152 let d = linear_dist(self, rgb);
153 if d < best_d {
154 best_d = d;
155 best = ansi;
156 }
157 }
158 best
159 }
160}
161
162fn linear_dist(a: Rgb, b: Rgb) -> f32 {
163 let la = a.linear();
164 let lb = b.linear();
165 let dr = la[0] - lb[0];
166 let dg = la[1] - lb[1];
167 let db = la[2] - lb[2];
168 dr * dr + dg * dg + db * db
169}
170
171impl From<irodori::Color> for Rgb {
172 fn from(c: irodori::Color) -> Self {
173 Rgb::from_color(c)
174 }
175}
176
177#[derive(Clone, Copy, PartialEq, Eq, Debug)]
179pub struct Capability {
180 pub level: ColorLevel,
181 pub width: u16,
183 pub is_tty: bool,
184 pub unicode: bool,
185 pub background: Background,
186}
187
188#[derive(Clone, Copy, PartialEq, Eq, Debug)]
189pub enum Stream {
190 Stdout,
191 Stderr,
192}
193
194impl Capability {
195 #[must_use]
199 pub fn probe() -> Self {
200 Self::probe_stream(Stream::Stdout)
201 }
202
203 #[must_use]
204 pub fn probe_stream(stream: Stream) -> Self {
205 use std::io::IsTerminal;
206 let is_tty = match stream {
207 Stream::Stdout => std::io::stdout().is_terminal(),
208 Stream::Stderr => std::io::stderr().is_terminal(),
209 };
210 let level = resolve_level(is_tty);
211 Self {
212 level,
213 width: probe_width(),
214 is_tty,
215 unicode: probe_unicode(),
216 background: Background::Assumed,
217 }
218 }
219
220 #[must_use]
222 pub const fn plain() -> Self {
223 Self {
224 level: ColorLevel::None,
225 width: 80,
226 is_tty: false,
227 unicode: false,
228 background: Background::Assumed,
229 }
230 }
231
232 #[must_use]
234 pub const fn fixed(level: ColorLevel, width: u16, unicode: bool) -> Self {
235 Self {
236 level,
237 width,
238 is_tty: matches!(level, ColorLevel::Truecolor | ColorLevel::Ansi256 | ColorLevel::Ansi16),
239 unicode,
240 background: Background::Assumed,
241 }
242 }
243}
244
245fn env_set(name: &str) -> bool {
246 std::env::var_os(name).is_some_and(|v| !v.is_empty())
247}
248
249fn resolve_level(is_tty: bool) -> ColorLevel {
250 let forced = env_set("CLICOLOR_FORCE");
252 if env_set("NO_COLOR") && !forced {
253 return ColorLevel::None;
254 }
255 if !is_tty && !forced {
256 return ColorLevel::None;
257 }
258 if let Ok(ct) = std::env::var("COLORTERM") {
259 if ct.contains("truecolor") || ct.contains("24bit") {
260 return ColorLevel::Truecolor;
261 }
262 }
263 match std::env::var("TERM") {
264 Ok(t) if t.contains("256") => ColorLevel::Ansi256,
265 Ok(t) if t.is_empty() || t == "dumb" => {
266 if forced {
267 ColorLevel::Ansi16
268 } else {
269 ColorLevel::None
270 }
271 }
272 Ok(_) => ColorLevel::Ansi16,
273 Err(_) => {
274 if forced {
275 ColorLevel::Ansi16
276 } else {
277 ColorLevel::None
278 }
279 }
280 }
281}
282
283fn probe_width() -> u16 {
284 if let Some((terminal_size::Width(w), _)) = terminal_size::terminal_size() {
285 return w.clamp(20, 400);
286 }
287 if let Ok(cols) = std::env::var("COLUMNS") {
288 if let Ok(w) = cols.parse::<u16>() {
289 return w.clamp(20, 400);
290 }
291 }
292 80
293}
294
295fn probe_unicode() -> bool {
296 for key in ["LC_ALL", "LC_CTYPE", "LANG"] {
297 if let Ok(v) = std::env::var(key) {
298 let v = v.to_ascii_lowercase();
299 if v.contains("utf-8") || v.contains("utf8") {
300 return true;
301 }
302 }
303 }
304 false
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn ansi256_cube_anchors() {
314 assert_eq!(Rgb::new(0, 0, 0).to_ansi256(), 16, "black is cube 16");
315 assert_eq!(Rgb::new(255, 255, 255).to_ansi256(), 231, "white is cube 231");
316 }
317
318 #[test]
319 fn ansi256_grey_ramp() {
320 let g = Rgb::new(128, 128, 128).to_ansi256();
322 assert!((232..=255).contains(&g), "mid grey -> ramp: {g}");
323 }
324
325 #[test]
326 fn ansi16_anchors() {
327 assert_eq!(Rgb::new(0, 0, 0).to_ansi16(), AnsiColor::Black);
328 assert_eq!(Rgb::new(255, 255, 255).to_ansi16(), AnsiColor::BrightWhite);
329 assert_eq!(Rgb::new(255, 0, 0).to_ansi16(), AnsiColor::BrightRed);
330 assert_eq!(Rgb::new(128, 0, 0).to_ansi16(), AnsiColor::Red);
331 }
332
333 #[test]
334 fn from_irodori_is_lossless() {
335 let n11 = irodori::NORD.aurora[0]; let rgb = Rgb::from_color(n11);
338 assert_eq!((rgb.r, rgb.g, rgb.b), (0xBF, 0x61, 0x6A));
339 }
340
341 #[test]
342 fn plain_capability_is_the_pipe_floor() {
343 let c = Capability::plain();
344 assert_eq!(c.level, ColorLevel::None);
345 assert!(!c.is_tty);
346 }
347}