Skip to main content

max / shop

9.3 KB · 302 lines History Blame Raw
1 //! SGR: turning `CSI Pm m` into the pending colours and attributes the print
2 //! path stamps onto every cell.
3 //!
4 //! Both spellings of extended colour are handled here: the semicolon form,
5 //! which eats following parameter groups, and the colon form, which arrives as
6 //! subparameters of one group.
7
8 use crate::{Attrs, Color, Grid};
9 use shop_vt::Params;
10
11 impl Grid {
12 pub(crate) fn apply_sgr(&mut self, params: &Params) {
13 // Bare `\e[m` is the same as `\e[0m` — full reset.
14 if params.is_empty() {
15 self.pending_fg = Color::Default;
16 self.pending_bg = Color::Default;
17 self.pending_attrs = Attrs::default();
18 self.recompute_style_words();
19 return;
20 }
21
22 // Collect param-group references so we can distinguish colon-form
23 // (subparams grouped) from semicolon-form (separate groups).
24 let groups: Vec<&[u16]> = params.iter().collect();
25 let mut i = 0;
26 while i < groups.len() {
27 let group = groups[i];
28 if group.len() > 1 {
29 // Colon form — the whole group is one logical SGR.
30 self.apply_colon_group(group);
31 i += 1;
32 continue;
33 }
34 let p = group.first().copied().unwrap_or(0);
35 i += self.apply_single(p, &groups, i);
36 }
37 self.recompute_style_words();
38 }
39
40 /// Semicolon-form: `p` came from a single-subparam group. Returns how many
41 /// group indices were consumed (usually 1, or up to 5 for extended color).
42 fn apply_single(&mut self, p: u16, groups: &[&[u16]], i: usize) -> usize {
43 match p {
44 0 => {
45 self.pending_fg = Color::Default;
46 self.pending_bg = Color::Default;
47 self.pending_attrs = Attrs::default();
48 1
49 }
50 1 => {
51 self.pending_attrs.bold = true;
52 1
53 }
54 2 => {
55 self.pending_attrs.dim = true;
56 1
57 }
58 3 => {
59 self.pending_attrs.italic = true;
60 1
61 }
62 4 => {
63 self.pending_attrs.underline = true;
64 1
65 }
66 7 => {
67 self.pending_attrs.reverse = true;
68 1
69 }
70 9 => {
71 self.pending_attrs.strikethrough = true;
72 1
73 }
74 22 => {
75 self.pending_attrs.bold = false;
76 self.pending_attrs.dim = false;
77 1
78 }
79 23 => {
80 self.pending_attrs.italic = false;
81 1
82 }
83 24 => {
84 self.pending_attrs.underline = false;
85 1
86 }
87 27 => {
88 self.pending_attrs.reverse = false;
89 1
90 }
91 29 => {
92 self.pending_attrs.strikethrough = false;
93 1
94 }
95 30..=37 => {
96 self.pending_fg = Color::Named((p - 30) as u8);
97 1
98 }
99 38 => {
100 let (color, consumed) = take_semi_extended(groups, i + 1);
101 if let Some(c) = color {
102 self.pending_fg = c;
103 }
104 1 + consumed
105 }
106 39 => {
107 self.pending_fg = Color::Default;
108 1
109 }
110 40..=47 => {
111 self.pending_bg = Color::Named((p - 40) as u8);
112 1
113 }
114 48 => {
115 let (color, consumed) = take_semi_extended(groups, i + 1);
116 if let Some(c) = color {
117 self.pending_bg = c;
118 }
119 1 + consumed
120 }
121 49 => {
122 self.pending_bg = Color::Default;
123 1
124 }
125 90..=97 => {
126 self.pending_fg = Color::Named(((p - 90) + 8) as u8);
127 1
128 }
129 100..=107 => {
130 self.pending_bg = Color::Named(((p - 100) + 8) as u8);
131 1
132 }
133 _ => 1,
134 }
135 }
136
137 /// Colon-form group: leading value is the SGR code, subsequent subparams
138 /// carry the extended-color payload. We only recognize 38 (fg) and 48 (bg)
139 /// here — colon-form underline color is a follow-up.
140 fn apply_colon_group(&mut self, group: &[u16]) {
141 let leading = group[0];
142 let color = parse_colon_extended(&group[1..]);
143 match leading {
144 38 => {
145 if let Some(c) = color {
146 self.pending_fg = c;
147 }
148 }
149 48 => {
150 if let Some(c) = color {
151 self.pending_bg = c;
152 }
153 }
154 _ => {}
155 }
156 }
157 }
158
159 /// Parse extended color from the following semicolon-separated groups. Handles
160 /// `2;R;G;B` (RGB) and `5;N` (indexed). Returns the parsed color and the
161 /// number of groups consumed after the leading `38`/`48`.
162 fn take_semi_extended(groups: &[&[u16]], start: usize) -> (Option<Color>, usize) {
163 let Some(fmt) = groups.get(start).and_then(|g| g.first().copied()) else {
164 return (None, 0);
165 };
166 match fmt {
167 5 => {
168 let Some(idx) = groups.get(start + 1).and_then(|g| g.first().copied()) else {
169 return (None, 1);
170 };
171 (Some(Color::Indexed(idx.min(255) as u8)), 2)
172 }
173 2 => {
174 let r = groups
175 .get(start + 1)
176 .and_then(|g| g.first().copied())
177 .unwrap_or(0);
178 let g = groups
179 .get(start + 2)
180 .and_then(|g| g.first().copied())
181 .unwrap_or(0);
182 let b = groups
183 .get(start + 3)
184 .and_then(|g| g.first().copied())
185 .unwrap_or(0);
186 (
187 Some(Color::Rgb(
188 r.min(255) as u8,
189 g.min(255) as u8,
190 b.min(255) as u8,
191 )),
192 4,
193 )
194 }
195 _ => (None, 1),
196 }
197 }
198
199 /// Parse extended color from a colon-form subparam tail — the bytes after the
200 /// leading `38`/`48`. `2;colorspace;R;G;B` (5 items) OR `2;R;G;B` (4 items)
201 /// OR `5;N` (2 items). Colorspace slot is skipped when present.
202 fn parse_colon_extended(rest: &[u16]) -> Option<Color> {
203 match rest.first().copied()? {
204 5 => rest.get(1).map(|&i| Color::Indexed(i.min(255) as u8)),
205 2 => match rest.len() {
206 // [2, colorspace, R, G, B]
207 5 => Some(Color::Rgb(
208 rest[2].min(255) as u8,
209 rest[3].min(255) as u8,
210 rest[4].min(255) as u8,
211 )),
212 // [2, R, G, B]
213 4 => Some(Color::Rgb(
214 rest[1].min(255) as u8,
215 rest[2].min(255) as u8,
216 rest[3].min(255) as u8,
217 )),
218 _ => None,
219 },
220 _ => None,
221 }
222 }
223
224 #[cfg(test)]
225 mod tests {
226 use crate::testutil::feed;
227 use crate::*;
228
229 // ---- SGR -----------------------------------------------------------
230
231 #[test]
232 fn sgr_named_fg_and_bg() {
233 let mut g = Grid::new(10, 1);
234 feed(&mut g, b"\x1b[31;44mA");
235 let cell = g.row(0)[0];
236 assert_eq!(cell.fg(), Color::Named(1)); // red
237 assert_eq!(cell.bg(), Color::Named(4)); // blue
238 }
239
240 #[test]
241 fn sgr_bright_named() {
242 let mut g = Grid::new(10, 1);
243 feed(&mut g, b"\x1b[92mA");
244 assert_eq!(g.row(0)[0].fg(), Color::Named(10)); // bright green = 8+2
245 }
246
247 #[test]
248 fn sgr_indexed_256() {
249 let mut g = Grid::new(10, 1);
250 feed(&mut g, b"\x1b[38;5;123mA");
251 assert_eq!(g.row(0)[0].fg(), Color::Indexed(123));
252 }
253
254 #[test]
255 fn sgr_truecolor_rgb() {
256 let mut g = Grid::new(10, 1);
257 feed(&mut g, b"\x1b[38;2;255;128;0mA");
258 assert_eq!(g.row(0)[0].fg(), Color::Rgb(255, 128, 0));
259 }
260
261 #[test]
262 fn sgr_colon_subparam_rgb() {
263 // The ITU-T `:` form: `\e[38:2::255:128:0m` — one param with
264 // subparams. Our SGR parser flattens both forms.
265 let mut g = Grid::new(10, 1);
266 feed(&mut g, b"\x1b[38:2::255:128:0mA");
267 assert_eq!(g.row(0)[0].fg(), Color::Rgb(255, 128, 0));
268 }
269
270 #[test]
271 fn sgr_attrs_bold_italic_underline() {
272 let mut g = Grid::new(10, 1);
273 feed(&mut g, b"\x1b[1;3;4mA");
274 let a = g.row(0)[0].attrs();
275 assert!(a.bold && a.italic && a.underline);
276 }
277
278 #[test]
279 fn sgr_reset_clears_everything() {
280 let mut g = Grid::new(10, 1);
281 feed(&mut g, b"\x1b[1;31;44mA\x1b[mB");
282 let a = g.row(0)[0];
283 let b = g.row(0)[1];
284 assert_eq!(a.fg(), Color::Named(1));
285 assert!(a.attrs().bold);
286 assert_eq!(b.fg(), Color::Default);
287 assert_eq!(b.bg(), Color::Default);
288 assert!(!b.attrs().bold);
289 }
290
291 #[test]
292 fn sgr_selective_clears() {
293 let mut g = Grid::new(10, 1);
294 feed(&mut g, b"\x1b[1;3mA\x1b[22mB\x1b[23mC");
295 assert!(g.row(0)[0].attrs().bold && g.row(0)[0].attrs().italic);
296 // 22 clears bold + dim; italic stays.
297 assert!(!g.row(0)[1].attrs().bold && g.row(0)[1].attrs().italic);
298 // 23 clears italic.
299 assert!(!g.row(0)[2].attrs().italic);
300 }
301 }
302