1use super::super::terminal::cell::Color;
4use crate::terminal::emu::Emulator;
5
6pub const DEFAULT: &str = "default";
8
9#[derive(Debug, Clone)]
10pub enum Expected {
11 Default,
13 Ansi256(u8),
14 Hex(u8, u8, u8),
15 Rgb(u8, u8, u8),
16}
17
18impl Expected {
19 pub fn parse(s: &str) -> anyhow::Result<Self> {
20 let s = s.trim();
21 if s.eq_ignore_ascii_case(DEFAULT) {
22 return Ok(Expected::Default);
23 }
24 if let Some(hex) = s.strip_prefix('#') {
25 let (r, g, b) = parse_hex(hex).map_err(|_| invalid(s))?;
26 return Ok(Expected::Hex(r, g, b));
27 }
28 if s.contains(',') {
29 let parts: Vec<&str> = s.split(',').map(|p| p.trim()).collect();
30 let parsed: Result<Vec<u8>, _> = parts.iter().map(|p| p.parse::<u8>()).collect();
31 match parsed.ok().as_deref() {
32 Some([r, g, b]) => return Ok(Expected::Rgb(*r, *g, *b)),
33 _ => return Err(invalid(s)),
34 }
35 }
36 let n: u8 = s.parse().map_err(|_| invalid(s))?;
37 Ok(Expected::Ansi256(n))
38 }
39
40 pub fn describe(&self) -> String {
41 match self {
42 Expected::Default => DEFAULT.to_string(),
43 Expected::Ansi256(n) => n.to_string(),
44 Expected::Hex(r, g, b) => format!("#{r:02x}{g:02x}{b:02x}"),
45 Expected::Rgb(r, g, b) => format!("{r},{g},{b}"),
46 }
47 }
48}
49
50fn invalid(got: &str) -> anyhow::Error {
52 anyhow::anyhow!(
53 "color must be \"{DEFAULT}\", ansi256 (0-255), hex (#rrggbb), or rgb (r,g,b) (got: \"{got}\")"
54 )
55}
56
57fn parse_hex(hex: &str) -> anyhow::Result<(u8, u8, u8)> {
58 if hex.len() != 6 {
59 anyhow::bail!("hex color must be 6 digits");
60 }
61 let r = u8::from_str_radix(&hex[0..2], 16)?;
62 let g = u8::from_str_radix(&hex[2..4], 16)?;
63 let b = u8::from_str_radix(&hex[4..6], 16)?;
64 Ok((r, g, b))
65}
66
67pub fn matches(
78 cell: Option<Color>,
79 expected: &Expected,
80 colors: &dyn Emulator,
81 is_fg: bool,
82) -> bool {
83 match expected {
84 Expected::Default => cell.is_none(),
85 Expected::Ansi256(n) => cell.is_some_and(|cell| cell.to_index() == *n),
86 Expected::Hex(er, eg, eb) | Expected::Rgb(er, eg, eb) => {
87 let got = colors.resolve(cell, is_fg);
88 (got.r, got.g, got.b) == (*er, *eg, *eb)
89 }
90 }
91}
92
93pub fn describe_cell(
95 cell: Option<Color>,
96 expected: &Expected,
97 colors: &dyn Emulator,
98 is_fg: bool,
99) -> String {
100 match expected {
101 Expected::Default => cell
102 .map(|cell| cell.to_index().to_string())
103 .unwrap_or_else(|| DEFAULT.to_string()),
104 Expected::Ansi256(_) => cell
105 .map(|cell| cell.to_index().to_string())
106 .unwrap_or_else(|| DEFAULT.to_string()),
107 _ => colors.resolve(cell, is_fg).to_hex(),
108 }
109}
110
111pub fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
112 if r == g && g == b {
113 if r < 8 {
114 return 16;
115 }
116 if r > 248 {
117 return 231;
118 }
119 return (232 + ((r as i32 - 8) * 24 / 247)) as u8;
120 }
121 let cube = |v: u8| -> i32 {
122 if v < 48 {
123 0
124 } else if v < 115 {
125 1
126 } else {
127 (v as i32 - 35) / 40
128 }
129 };
130 (16 + 36 * cube(r) + 6 * cube(g) + cube(b)) as u8
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::profile::{Colors, Profile};
137 use crate::terminal::alacritty::AlacrittyEmu;
138 use crate::terminal::cell::Color;
139 use crate::terminal::emu::Emulator;
140
141 fn emu_with(colors: Colors) -> AlacrittyEmu {
144 AlacrittyEmu::new(
145 10,
146 2,
147 &Profile {
148 colors,
149 ..Default::default()
150 },
151 )
152 }
153
154 #[test]
155 fn parse_forms() {
156 assert!(matches!(
157 Expected::parse("9").unwrap(),
158 Expected::Ansi256(9)
159 ));
160 assert!(matches!(
161 Expected::parse("#ff0000").unwrap(),
162 Expected::Hex(255, 0, 0)
163 ));
164 assert!(matches!(
165 Expected::parse("255,0,0").unwrap(),
166 Expected::Rgb(255, 0, 0)
167 ));
168 }
169
170 #[test]
171 fn matches_palette_and_default() {
172 let c = emu_with(Colors::default());
173 let idx = |i| Some(Color::from_index(i));
174 assert!(matches(idx(9), &Expected::Ansi256(9), &c, true));
175 assert!(!matches(idx(2), &Expected::Ansi256(9), &c, true));
176 assert!(matches(idx(196), &Expected::Ansi256(196), &c, true));
177 assert!(matches(
178 Some(Color::Rgb(255, 0, 0)),
179 &Expected::Rgb(255, 0, 0),
180 &c,
181 true
182 ));
183 }
184
185 #[test]
186 fn default_color_matches_its_resolved_foreground_or_background() {
187 let c = emu_with(Colors::default());
188 assert!(!matches(None, &Expected::Ansi256(0), &c, true));
189 assert!(matches(None, &Expected::Hex(192, 192, 192), &c, true));
190 assert!(matches(None, &Expected::Hex(0, 0, 0), &c, false));
191 assert!(!matches(None, &Expected::Hex(0, 0, 0), &c, true));
192 assert!(matches(None, &Expected::Default, &c, true));
193 assert_eq!(
194 describe_cell(None, &Expected::Hex(0, 0, 0), &c, true),
195 "#c0c0c0"
196 );
197 }
198
199 #[test]
200 fn default_color_assertions_follow_runtime_changes() {
201 let mut c = emu_with(Colors::default());
202 c.process(b"\x1b]10;#010203\x07\x1b]11;#040506\x07");
203 assert!(matches(None, &Expected::Hex(1, 2, 3), &c, true));
204 assert!(matches(None, &Expected::Hex(4, 5, 6), &c, false));
205 }
206
207 #[test]
208 fn a_colored_cell_is_not_default() {
209 let c = emu_with(Colors::default());
210 let red = Some(Color::from_index(1));
211 assert!(!matches(red, &Expected::Default, &c, true));
212 assert!(matches(red, &Expected::Ansi256(1), &c, true));
213 assert!(matches!(
214 Expected::parse("default").unwrap(),
215 Expected::Default
216 ));
217 assert!(matches!(
218 Expected::parse("DEFAULT").unwrap(),
219 Expected::Default
220 ));
221 assert_eq!(describe_cell(red, &Expected::Default, &c, true), "1");
222 }
223
224 #[test]
228 fn an_assertion_matches_the_color_a_screenshot_paints() {
229 let colors = emu_with(Colors::default());
230 for index in 0u8..=255 {
231 let cell = Some(Color::from_index(index));
232 let painted = colors.resolve(cell, true);
233 assert!(
234 matches(
235 cell,
236 &Expected::Hex(painted.r, painted.g, painted.b),
237 &colors,
238 true
239 ),
240 "slot {index} paints {} but does not match it",
241 painted.to_hex()
242 );
243 }
244 }
245
246 #[test]
253 fn an_assertion_follows_a_color_a_program_set() {
254 use crate::terminal::emu::Emulator;
255 let mut emu = emu_with(Colors::default());
256 let red = Some(Color::from_index(1));
257 let configured = Colors::default().red;
258
259 assert!(matches(
260 red,
261 &Expected::Hex(configured.r, configured.g, configured.b),
262 &emu,
263 true
264 ));
265
266 emu.process(b"\x1b]4;1;#22c55e\x07");
267 assert!(
268 matches(red, &Expected::Hex(0x22, 0xc5, 0x5e), &emu, true),
269 "the assertion follows the colour the program set"
270 );
271 assert!(
272 !matches(
273 red,
274 &Expected::Hex(configured.r, configured.g, configured.b),
275 &emu,
276 true
277 ),
278 "the configured colour is no longer what slot 1 shows"
279 );
280 assert!(
281 matches(red, &Expected::Ansi256(1), &emu, true),
282 "the index is unaffected: it names a slot, not a colour"
283 );
284
285 emu.process(b"\x1b]104;1\x07");
286 assert!(
287 matches(
288 red,
289 &Expected::Hex(configured.r, configured.g, configured.b),
290 &emu,
291 true
292 ),
293 "a reset restores the configured colour"
294 );
295 }
296
297 #[test]
300 fn a_recolored_profile_moves_what_an_assertion_matches() {
301 let colors = emu_with(Colors {
302 red: crate::profile::Rgb::new(1, 2, 3),
303 ..Default::default()
304 });
305 let red = Some(Color::from_index(1));
306 assert!(matches(red, &Expected::Hex(1, 2, 3), &colors, true));
307 assert!(!matches(red, &Expected::Hex(128, 0, 0), &colors, true));
308 assert!(
309 matches(red, &Expected::Ansi256(1), &colors, true),
310 "the index is unaffected by the palette"
311 );
312 }
313}