termprofile/convert/
mod.rs1mod adapt;
2mod ansi_256_to_16;
3mod ansi_256_to_rgb;
4mod color;
5#[cfg(feature = "ratatui")]
6mod ratatui;
7
8pub use adapt::*;
9use ansi_256_to_16::ANSI_256_TO_16;
10use ansi_256_to_rgb::ANSI_256_TO_RGB;
11use anstyle::{Ansi256Color, AnsiColor, RgbColor};
12pub use color::*;
13use palette::Srgb;
14
15use crate::TermProfile;
16
17impl TermProfile {
18 pub fn adapt_color<C>(&self, color: C) -> Option<C>
20 where
21 C: AdaptableColor,
22 {
23 if *self < Self::Ansi16 {
24 return None;
25 }
26 if color.as_ansi_16().is_some() {
27 Some(color)
28 } else if let Some(index) = color.as_ansi_256() {
29 if *self >= Self::Ansi256 {
30 Some(color)
31 } else {
32 Some(C::from_ansi_16(ansi256_to_ansi16(index.0)))
33 }
34 } else if let Some(rgb_color) = color.as_rgb() {
35 if *self == Self::TrueColor {
36 Some(color)
37 } else {
38 let ansi256_index = rgb_to_ansi256(rgb_color);
39 if *self == Self::Ansi256 {
40 Some(C::from_ansi_256(ansi256_index.into()))
41 } else {
42 Some(C::from_ansi_16(ansi256_to_ansi16(ansi256_index)))
43 }
44 }
45 } else {
46 Some(color)
47 }
48 }
49
50 pub fn adapt_style<S>(&self, mut style: S) -> S
52 where
53 S: AdaptableStyle,
54 {
55 if *self == Self::NoTty {
56 return S::default();
57 }
58 if let Some(color) = style.get_fg_color() {
59 style = style.fg_color(self.adapt_color(color));
60 }
61 if let Some(color) = style.get_bg_color() {
62 style = style.bg_color(self.adapt_color(color));
63 }
64 if let Some(color) = style.get_underline_color() {
65 style = style.underline_color(self.adapt_color(color));
66 }
67 style
68 }
69}
70
71pub fn ansi256_to_ansi16(ansi256_index: u8) -> AnsiColor {
73 match ANSI_256_TO_16[&ansi256_index] {
74 0 => AnsiColor::Black,
75 1 => AnsiColor::Red,
76 2 => AnsiColor::Green,
77 3 => AnsiColor::Yellow,
78 4 => AnsiColor::Blue,
79 5 => AnsiColor::Magenta,
80 6 => AnsiColor::Cyan,
81 7 => AnsiColor::White,
82 8 => AnsiColor::BrightBlack,
83 9 => AnsiColor::BrightRed,
84 10 => AnsiColor::BrightGreen,
85 11 => AnsiColor::BrightYellow,
86 12 => AnsiColor::BrightBlue,
87 13 => AnsiColor::BrightMagenta,
88 14 => AnsiColor::BrightCyan,
89 15 => AnsiColor::BrightWhite,
90 _ => unreachable!(),
91 }
92}
93
94#[cfg(feature = "color-cache")]
95static COLOR_CACHE: std::sync::LazyLock<std::sync::Mutex<lru::LruCache<RgbColor, u8>>> =
96 std::sync::LazyLock::new(|| lru::LruCache::new(256.try_into().expect("invalid size")).into());
97
98#[cfg(feature = "color-cache")]
99static CACHE_ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
100
101#[cfg(feature = "color-cache")]
103pub fn set_color_cache_enabled(enabled: bool) {
104 CACHE_ENABLED.store(enabled, std::sync::atomic::Ordering::SeqCst);
105}
106
107#[cfg(feature = "color-cache")]
113pub fn set_color_cache_size(size: std::num::NonZeroUsize) {
114 COLOR_CACHE.lock().expect("lock poisoned").resize(size);
115}
116
117#[cfg(feature = "color-cache")]
123pub fn rgb_to_ansi256(color: RgbColor) -> u8 {
124 if CACHE_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
125 if let Some(cached) = COLOR_CACHE.lock().expect("lock poisoned").get(&color) {
126 return *cached;
127 }
128 let converted = rgb_to_ansi256_inner(color);
129 COLOR_CACHE
130 .lock()
131 .expect("lock poisoned")
132 .put(color, converted);
133 converted
134 } else {
135 rgb_to_ansi256_inner(color)
136 }
137}
138
139#[cfg(not(feature = "color-cache"))]
141pub fn rgb_to_ansi256(color: RgbColor) -> u8 {
142 rgb_to_ansi256_inner(color)
143}
144
145fn get_color_index<const N: usize>(val: u8, breakpoints: [u8; N]) -> usize {
146 breakpoints.iter().position(|p| val < *p).unwrap_or(N)
147}
148
149fn red_color_index(val: u8) -> usize {
152 get_color_index(val, [49, 116, 156, 196, 236])
153}
154
155fn green_color_index(val: u8) -> usize {
156 get_color_index(val, [48, 116, 156, 196, 236])
157}
158
159fn blue_color_index(val: u8) -> usize {
160 get_color_index(val, [48, 116, 156, 196, 236])
161}
162
163const COLOR_INTERVALS: [u8; 6] = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
164
165fn rgb_to_ansi256_inner(color: RgbColor) -> u8 {
169 let srgb = Srgb::new(color.r(), color.g(), color.b());
170
171 let qr = red_color_index(srgb.red);
172 let qg = green_color_index(srgb.green);
173 let qb = blue_color_index(srgb.blue);
174 let cr = COLOR_INTERVALS[qr];
175 let cg = COLOR_INTERVALS[qg];
176 let cb = COLOR_INTERVALS[qb];
177 let color_index = (36 * qr + 6 * qg + qb + 16) as u8;
178
179 if cr == srgb.red && cg == srgb.green && cb == srgb.blue {
180 return color_index;
181 }
182 let average = ((srgb.red as u32 + srgb.green as u32 + srgb.blue as u32) / 3) as u8;
183 let gray_index = if average > 238 {
184 23
185 } else {
186 (average.saturating_sub(3)) / 10
187 };
188 let gray_value = 8 + 10 * gray_index;
189
190 let color2 = Srgb::new(cr, cg, cb);
191 let gray2 = Srgb::new(gray_value, gray_value, gray_value);
192
193 let color_distance = distance_squared(srgb, color2);
194 let gray_distance = distance_squared(srgb, gray2);
195 if color_distance <= gray_distance {
196 color_index
197 } else {
198 232 + gray_index
199 }
200}
201
202pub fn ansi256_to_rgb(ansi: Ansi256Color) -> RgbColor {
204 ANSI_256_TO_RGB[ansi.0 as usize]
205}
206
207fn distance_squared(rgb1: Srgb<u8>, rgb2: Srgb<u8>) -> u32 {
214 let r_mean = (rgb1.red as i32 + rgb2.red as i32) / 2;
215 let r = (rgb1.red as i32) - (rgb2.red as i32);
216 let g = (rgb1.green as i32) - (rgb2.green as i32);
217 let b = (rgb1.blue as i32) - (rgb2.blue as i32);
218 ((((512 + r_mean) * r * r) >> 8) + 4 * g * g + (((767 - r_mean) * b * b) >> 8)) as u32
219}
220
221#[cfg(test)]
222#[path = "./convert_test.rs"]
223mod convert_test;