use super::grid::Cell;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(super) enum Color {
#[default]
Default,
Indexed(u8),
Rgb(u8, u8, u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[allow(
clippy::struct_excessive_bools,
reason = "each flag is an independent SGR attribute"
)]
pub(super) struct Sgr {
fg: Color,
bg: Color,
bold: bool,
dim: bool,
italic: bool,
underline: bool,
reverse: bool,
}
const RESET: &str = "\x1b[0m";
fn byte(value: u16) -> u8 {
u8::try_from(value).unwrap_or(u8::MAX)
}
impl Sgr {
pub(super) fn erased(self) -> Self {
Self {
bg: self.bg,
..Self::default()
}
}
pub(super) fn apply(&mut self, params: &vte::Params) {
if params.is_empty() {
*self = Self::default();
return;
}
let mut iter = params.iter();
while let Some(param) = iter.next() {
let code = param.first().copied().unwrap_or(0);
match code {
0 => *self = Self::default(),
1 => self.bold = true,
2 => self.dim = true,
3 => self.italic = true,
4 => self.underline = true,
7 => self.reverse = true,
22 => {
self.bold = false;
self.dim = false;
}
23 => self.italic = false,
24 => self.underline = false,
27 => self.reverse = false,
30..=37 => self.fg = Color::Indexed(byte(code - 30)),
39 => self.fg = Color::Default,
40..=47 => self.bg = Color::Indexed(byte(code - 40)),
49 => self.bg = Color::Default,
90..=97 => self.fg = Color::Indexed(byte(code - 90 + 8)),
100..=107 => self.bg = Color::Indexed(byte(code - 100 + 8)),
38 => {
if let Some(color) = extended_color(param, &mut iter) {
self.fg = color;
}
}
48 => {
if let Some(color) = extended_color(param, &mut iter) {
self.bg = color;
}
}
_ => {}
}
}
}
fn escape(&self) -> String {
let mut codes: Vec<String> = vec!["0".to_string()];
if self.bold {
codes.push("1".to_string());
}
if self.dim {
codes.push("2".to_string());
}
if self.italic {
codes.push("3".to_string());
}
if self.underline {
codes.push("4".to_string());
}
if self.reverse {
codes.push("7".to_string());
}
push_color(&mut codes, self.fg, false);
push_color(&mut codes, self.bg, true);
format!("\x1b[{}m", codes.join(";"))
}
}
fn push_color(codes: &mut Vec<String>, color: Color, background: bool) {
let (base, bright_base, extended) = if background {
(40u16, 100u16, 48u16)
} else {
(30, 90, 38)
};
match color {
Color::Default => {}
Color::Indexed(index @ 0..=7) => codes.push((base + u16::from(index)).to_string()),
Color::Indexed(index @ 8..=15) => {
codes.push((bright_base + u16::from(index) - 8).to_string());
}
Color::Indexed(index) => {
codes.push(format!("{extended};5;{index}"));
}
Color::Rgb(r, g, b) => codes.push(format!("{extended};2;{r};{g};{b}")),
}
}
fn extended_color<'a>(param: &[u16], iter: &mut impl Iterator<Item = &'a [u16]>) -> Option<Color> {
if param.len() > 1 {
return match param.get(1)? {
5 => param.get(2).map(|&index| Color::Indexed(byte(index))),
2 => {
let len = param.len();
if len < 5 {
return None;
}
Some(Color::Rgb(
byte(param[len - 3]),
byte(param[len - 2]),
byte(param[len - 1]),
))
}
_ => None,
};
}
match iter.next()?.first()? {
5 => iter
.next()
.and_then(<[u16]>::first)
.map(|&index| Color::Indexed(byte(index))),
2 => {
let r = byte(*iter.next()?.first()?);
let g = byte(*iter.next()?.first()?);
let b = byte(*iter.next()?.first()?);
Some(Color::Rgb(r, g, b))
}
_ => None,
}
}
pub(super) fn render_row(cells: &[Cell]) -> String {
let end = cells
.iter()
.rposition(|cell| !cell.is_blank())
.map_or(0, |index| index + 1);
let mut out = String::new();
let mut current = Sgr::default();
for cell in &cells[..end] {
if cell.sgr != current {
out.push_str(&cell.sgr.escape());
current = cell.sgr;
}
out.push(cell.ch);
}
if current != Sgr::default() {
out.push_str(RESET);
}
out
}
#[cfg(test)]
mod tests {
use super::super::grid::Cell;
use super::{Color, Sgr, render_row};
fn styled_cells(input: &[u8]) -> Vec<Cell> {
let mut parser = vte::Parser::new();
let mut row = Row::default();
parser.advance(&mut row, input);
row.cells
}
#[derive(Default)]
struct Row {
sgr: Sgr,
cells: Vec<Cell>,
}
impl vte::Perform for Row {
fn print(&mut self, c: char) {
self.cells.push(Cell {
ch: c,
sgr: self.sgr,
});
}
fn csi_dispatch(
&mut self,
params: &vte::Params,
_intermediates: &[u8],
_ignore: bool,
action: char,
) {
if action == 'm' {
self.sgr.apply(params);
}
}
}
#[test]
fn parses_basic_colors_and_attributes() {
let cells = styled_cells(b"\x1b[1;31mA\x1b[0mB");
assert_eq!(cells[0].sgr.fg, Color::Indexed(1));
assert!(cells[0].sgr.bold);
assert_eq!(cells[1].sgr, Sgr::default());
}
#[test]
fn parses_256_and_truecolor_in_both_forms() {
assert_eq!(
styled_cells(b"\x1b[38;5;208mX")[0].sgr.fg,
Color::Indexed(208)
);
assert_eq!(
styled_cells(b"\x1b[38:5:208mX")[0].sgr.fg,
Color::Indexed(208)
);
assert_eq!(
styled_cells(b"\x1b[38;2;10;20;30mX")[0].sgr.fg,
Color::Rgb(10, 20, 30)
);
assert_eq!(
styled_cells(b"\x1b[38:2::10:20:30mX")[0].sgr.fg,
Color::Rgb(10, 20, 30)
);
}
#[test]
fn render_row_emits_minimal_transitions_and_no_redundant_reset() {
let cells = styled_cells(b"\x1b[31mAB\x1b[0mC");
assert_eq!(render_row(&cells), "\x1b[0;31mAB\x1b[0mC");
}
#[test]
fn render_row_closes_row_when_styling_remains_active() {
let cells = styled_cells(b"\x1b[31mAB");
assert_eq!(render_row(&cells), "\x1b[0;31mAB\x1b[0m");
}
#[test]
fn render_row_trims_trailing_blanks() {
let mut cells = styled_cells(b"hi");
cells.extend(std::iter::repeat_n(Cell::default(), 4));
assert_eq!(render_row(&cells), "hi");
}
#[test]
fn color_survives_round_trip() {
let cells = styled_cells(b"\x1b[38;5;123mZ");
let rendered = render_row(&cells);
let reparsed = styled_cells(rendered.as_bytes());
assert_eq!(reparsed[0].sgr.fg, Color::Indexed(123));
}
}