1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15#[non_exhaustive]
16pub enum ColorMode {
17 None,
19 #[default]
21 Ansi256,
22 TrueColor,
24}
25
26const TAG_SHIFT: u32 = 30;
27const TAG_ANSI: u32 = 1;
28const TAG_RGB: u32 = 2;
29
30#[derive(Clone, Copy, PartialEq, Eq, Default)]
32pub struct CellColor(u32);
33
34impl CellColor {
35 pub const DEFAULT: CellColor = CellColor(0);
37
38 pub const fn ansi256(index: u8) -> CellColor {
40 CellColor((TAG_ANSI << TAG_SHIFT) | index as u32)
41 }
42
43 pub const fn rgb(r: u8, g: u8, b: u8) -> CellColor {
45 CellColor((TAG_RGB << TAG_SHIFT) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32)
46 }
47
48 #[inline]
50 pub fn is_set(self) -> bool {
51 self.0 != 0
52 }
53
54 #[inline]
56 pub(crate) fn raw(self) -> u32 {
57 self.0
58 }
59
60 #[inline]
62 pub(crate) fn from_raw(raw: u32) -> CellColor {
63 CellColor(raw)
64 }
65
66 #[inline]
67 pub(crate) fn is_ansi(self) -> bool {
68 self.0 >> TAG_SHIFT == TAG_ANSI
69 }
70
71 #[inline]
72 pub(crate) fn is_rgb(self) -> bool {
73 self.0 >> TAG_SHIFT == TAG_RGB
74 }
75
76 #[inline]
78 pub(crate) fn ansi_index(self) -> u8 {
79 (self.0 & 0xFF) as u8
80 }
81
82 #[inline]
84 pub(crate) fn rgb_parts(self) -> (u8, u8, u8) {
85 (
86 ((self.0 >> 16) & 0xFF) as u8,
87 ((self.0 >> 8) & 0xFF) as u8,
88 (self.0 & 0xFF) as u8,
89 )
90 }
91
92 pub fn as_ansi256(self) -> Option<u8> {
96 if self.is_ansi() {
97 Some(self.ansi_index())
98 } else if self.is_rgb() {
99 let (r, g, b) = self.rgb_parts();
100 Some(rgb_to_ansi256(r, g, b))
101 } else {
102 None
103 }
104 }
105
106 pub fn as_rgb(self) -> Option<(u8, u8, u8)> {
111 if self.is_rgb() {
112 Some(self.rgb_parts())
113 } else if self.is_ansi() {
114 Some(ansi256_to_rgb(self.ansi_index()))
115 } else {
116 None
117 }
118 }
119}
120
121impl core::fmt::Debug for CellColor {
122 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
123 if !self.is_set() {
124 write!(f, "CellColor::DEFAULT")
125 } else if self.is_ansi() {
126 write!(f, "CellColor::ansi256({})", self.ansi_index())
127 } else {
128 let (r, g, b) = self.rgb_parts();
129 write!(f, "CellColor::rgb({r}, {g}, {b})")
130 }
131 }
132}
133
134pub(crate) fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
137 if r == g && g == b {
141 let v = r as i32;
142 let ramp_i = ((v - 8 + 5) / 10).clamp(0, 23);
143 let ramp_v = 8 + 10 * ramp_i;
144 let cube_q = if v < 48 {
145 0
146 } else if v < 115 {
147 1
148 } else {
149 ((v - 35) / 40).min(5)
150 };
151 let cube_v = if cube_q == 0 { 0 } else { 55 + 40 * cube_q };
152 return if (v - cube_v).abs() < (v - ramp_v).abs() {
153 (16 + 43 * cube_q) as u8 } else {
155 (232 + ramp_i) as u8
156 };
157 }
158 let q = |v: u8| -> u16 {
159 if v < 48 {
161 0
162 } else if v < 115 {
163 1
164 } else {
165 ((v as u16) - 35) / 40
166 }
167 };
168 (16 + 36 * q(r) + 6 * q(g) + q(b)) as u8
169}
170
171pub(crate) fn ansi256_to_rgb(index: u8) -> (u8, u8, u8) {
173 match index {
174 0 => (0, 0, 0),
176 1 => (205, 0, 0),
177 2 => (0, 205, 0),
178 3 => (205, 205, 0),
179 4 => (0, 0, 238),
180 5 => (205, 0, 205),
181 6 => (0, 205, 205),
182 7 => (229, 229, 229),
183 8 => (127, 127, 127),
184 9 => (255, 0, 0),
185 10 => (0, 255, 0),
186 11 => (255, 255, 0),
187 12 => (92, 92, 255),
188 13 => (255, 0, 255),
189 14 => (0, 255, 255),
190 15 => (255, 255, 255),
191 16..=231 => {
192 let i = index as u16 - 16;
194 let level = |c: u16| -> u8 { if c == 0 { 0 } else { (55 + 40 * c) as u8 } };
195 (level(i / 36), level((i / 6) % 6), level(i % 6))
196 }
197 232..=255 => {
198 let v = (8 + 10 * (index as u16 - 232)) as u8;
200 (v, v, v)
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn packing_roundtrips() {
211 assert!(!CellColor::DEFAULT.is_set());
212 let a = CellColor::ansi256(196);
213 assert!(a.is_set() && a.is_ansi());
214 assert_eq!(a.ansi_index(), 196);
215 let c = CellColor::rgb(12, 34, 56);
216 assert!(c.is_set() && c.is_rgb());
217 assert_eq!(c.rgb_parts(), (12, 34, 56));
218 }
219
220 #[test]
221 fn cube_expansion_matches_xterm() {
222 assert_eq!(ansi256_to_rgb(196), (255, 0, 0)); assert_eq!(ansi256_to_rgb(16), (0, 0, 0)); assert_eq!(ansi256_to_rgb(231), (255, 255, 255)); assert_eq!(ansi256_to_rgb(244), (128, 128, 128)); assert_eq!(ansi256_to_rgb(51), (0, 255, 255)); }
228
229 #[test]
230 fn quantization_hits_cube_corners() {
231 assert_eq!(rgb_to_ansi256(255, 0, 0), 196);
232 assert_eq!(rgb_to_ansi256(0, 255, 255), 51);
233 assert_eq!(rgb_to_ansi256(0, 0, 0), 16);
234 assert_eq!(rgb_to_ansi256(255, 255, 255), 231);
235 assert_eq!(rgb_to_ansi256(128, 128, 128), 244);
236 }
237
238 #[test]
239 fn quantize_expand_is_stable() {
240 for index in 16u16..=255 {
243 let (r, g, b) = ansi256_to_rgb(index as u8);
244 assert_eq!(
245 rgb_to_ansi256(r, g, b),
246 index as u8,
247 "index {index} not stable"
248 );
249 }
250 }
251
252 #[test]
253 fn mode_conversions() {
254 let red = CellColor::rgb(255, 0, 0);
255 assert_eq!(red.as_ansi256(), Some(196));
256 let ansi = CellColor::ansi256(196);
257 assert_eq!(ansi.as_rgb(), Some((255, 0, 0)));
258 assert_eq!(CellColor::DEFAULT.as_ansi256(), None);
259 assert_eq!(CellColor::DEFAULT.as_rgb(), None);
260 assert_eq!(ColorMode::default(), ColorMode::Ansi256);
261 }
262}