//! SGR: turning `CSI Pm m` into the pending colours and attributes the print //! path stamps onto every cell. //! //! Both spellings of extended colour are handled here: the semicolon form, //! which eats following parameter groups, and the colon form, which arrives as //! subparameters of one group. use crate::{Attrs, Color, Grid}; use shop_vt::Params; impl Grid { pub(crate) fn apply_sgr(&mut self, params: &Params) { // Bare `\e[m` is the same as `\e[0m` — full reset. if params.is_empty() { self.pending_fg = Color::Default; self.pending_bg = Color::Default; self.pending_attrs = Attrs::default(); self.recompute_style_words(); return; } // Collect param-group references so we can distinguish colon-form // (subparams grouped) from semicolon-form (separate groups). let groups: Vec<&[u16]> = params.iter().collect(); let mut i = 0; while i < groups.len() { let group = groups[i]; if group.len() > 1 { // Colon form — the whole group is one logical SGR. self.apply_colon_group(group); i += 1; continue; } let p = group.first().copied().unwrap_or(0); i += self.apply_single(p, &groups, i); } self.recompute_style_words(); } /// Semicolon-form: `p` came from a single-subparam group. Returns how many /// group indices were consumed (usually 1, or up to 5 for extended color). fn apply_single(&mut self, p: u16, groups: &[&[u16]], i: usize) -> usize { match p { 0 => { self.pending_fg = Color::Default; self.pending_bg = Color::Default; self.pending_attrs = Attrs::default(); 1 } 1 => { self.pending_attrs.bold = true; 1 } 2 => { self.pending_attrs.dim = true; 1 } 3 => { self.pending_attrs.italic = true; 1 } 4 => { self.pending_attrs.underline = true; 1 } 7 => { self.pending_attrs.reverse = true; 1 } 9 => { self.pending_attrs.strikethrough = true; 1 } 22 => { self.pending_attrs.bold = false; self.pending_attrs.dim = false; 1 } 23 => { self.pending_attrs.italic = false; 1 } 24 => { self.pending_attrs.underline = false; 1 } 27 => { self.pending_attrs.reverse = false; 1 } 29 => { self.pending_attrs.strikethrough = false; 1 } 30..=37 => { self.pending_fg = Color::Named((p - 30) as u8); 1 } 38 => { let (color, consumed) = take_semi_extended(groups, i + 1); if let Some(c) = color { self.pending_fg = c; } 1 + consumed } 39 => { self.pending_fg = Color::Default; 1 } 40..=47 => { self.pending_bg = Color::Named((p - 40) as u8); 1 } 48 => { let (color, consumed) = take_semi_extended(groups, i + 1); if let Some(c) = color { self.pending_bg = c; } 1 + consumed } 49 => { self.pending_bg = Color::Default; 1 } 90..=97 => { self.pending_fg = Color::Named(((p - 90) + 8) as u8); 1 } 100..=107 => { self.pending_bg = Color::Named(((p - 100) + 8) as u8); 1 } _ => 1, } } /// Colon-form group: leading value is the SGR code, subsequent subparams /// carry the extended-color payload. We only recognize 38 (fg) and 48 (bg) /// here — colon-form underline color is a follow-up. fn apply_colon_group(&mut self, group: &[u16]) { let leading = group[0]; let color = parse_colon_extended(&group[1..]); match leading { 38 => { if let Some(c) = color { self.pending_fg = c; } } 48 => { if let Some(c) = color { self.pending_bg = c; } } _ => {} } } } /// Parse extended color from the following semicolon-separated groups. Handles /// `2;R;G;B` (RGB) and `5;N` (indexed). Returns the parsed color and the /// number of groups consumed after the leading `38`/`48`. fn take_semi_extended(groups: &[&[u16]], start: usize) -> (Option, usize) { let Some(fmt) = groups.get(start).and_then(|g| g.first().copied()) else { return (None, 0); }; match fmt { 5 => { let Some(idx) = groups.get(start + 1).and_then(|g| g.first().copied()) else { return (None, 1); }; (Some(Color::Indexed(idx.min(255) as u8)), 2) } 2 => { let r = groups .get(start + 1) .and_then(|g| g.first().copied()) .unwrap_or(0); let g = groups .get(start + 2) .and_then(|g| g.first().copied()) .unwrap_or(0); let b = groups .get(start + 3) .and_then(|g| g.first().copied()) .unwrap_or(0); ( Some(Color::Rgb( r.min(255) as u8, g.min(255) as u8, b.min(255) as u8, )), 4, ) } _ => (None, 1), } } /// Parse extended color from a colon-form subparam tail — the bytes after the /// leading `38`/`48`. `2;colorspace;R;G;B` (5 items) OR `2;R;G;B` (4 items) /// OR `5;N` (2 items). Colorspace slot is skipped when present. fn parse_colon_extended(rest: &[u16]) -> Option { match rest.first().copied()? { 5 => rest.get(1).map(|&i| Color::Indexed(i.min(255) as u8)), 2 => match rest.len() { // [2, colorspace, R, G, B] 5 => Some(Color::Rgb( rest[2].min(255) as u8, rest[3].min(255) as u8, rest[4].min(255) as u8, )), // [2, R, G, B] 4 => Some(Color::Rgb( rest[1].min(255) as u8, rest[2].min(255) as u8, rest[3].min(255) as u8, )), _ => None, }, _ => None, } } #[cfg(test)] mod tests { use crate::testutil::feed; use crate::*; // ---- SGR ----------------------------------------------------------- #[test] fn sgr_named_fg_and_bg() { let mut g = Grid::new(10, 1); feed(&mut g, b"\x1b[31;44mA"); let cell = g.row(0)[0]; assert_eq!(cell.fg(), Color::Named(1)); // red assert_eq!(cell.bg(), Color::Named(4)); // blue } #[test] fn sgr_bright_named() { let mut g = Grid::new(10, 1); feed(&mut g, b"\x1b[92mA"); assert_eq!(g.row(0)[0].fg(), Color::Named(10)); // bright green = 8+2 } #[test] fn sgr_indexed_256() { let mut g = Grid::new(10, 1); feed(&mut g, b"\x1b[38;5;123mA"); assert_eq!(g.row(0)[0].fg(), Color::Indexed(123)); } #[test] fn sgr_truecolor_rgb() { let mut g = Grid::new(10, 1); feed(&mut g, b"\x1b[38;2;255;128;0mA"); assert_eq!(g.row(0)[0].fg(), Color::Rgb(255, 128, 0)); } #[test] fn sgr_colon_subparam_rgb() { // The ITU-T `:` form: `\e[38:2::255:128:0m` — one param with // subparams. Our SGR parser flattens both forms. let mut g = Grid::new(10, 1); feed(&mut g, b"\x1b[38:2::255:128:0mA"); assert_eq!(g.row(0)[0].fg(), Color::Rgb(255, 128, 0)); } #[test] fn sgr_attrs_bold_italic_underline() { let mut g = Grid::new(10, 1); feed(&mut g, b"\x1b[1;3;4mA"); let a = g.row(0)[0].attrs(); assert!(a.bold && a.italic && a.underline); } #[test] fn sgr_reset_clears_everything() { let mut g = Grid::new(10, 1); feed(&mut g, b"\x1b[1;31;44mA\x1b[mB"); let a = g.row(0)[0]; let b = g.row(0)[1]; assert_eq!(a.fg(), Color::Named(1)); assert!(a.attrs().bold); assert_eq!(b.fg(), Color::Default); assert_eq!(b.bg(), Color::Default); assert!(!b.attrs().bold); } #[test] fn sgr_selective_clears() { let mut g = Grid::new(10, 1); feed(&mut g, b"\x1b[1;3mA\x1b[22mB\x1b[23mC"); assert!(g.row(0)[0].attrs().bold && g.row(0)[0].attrs().italic); // 22 clears bold + dim; italic stays. assert!(!g.row(0)[1].attrs().bold && g.row(0)[1].attrs().italic); // 23 clears italic. assert!(!g.row(0)[2].attrs().italic); } }