#[derive(Debug, PartialEq, Eq)]
enum State {
Ground,
Esc,
Csi,
}
pub(super) struct AttrShadow {
parser: ::vt100::Parser,
state: State,
pending: Vec<u8>,
}
impl AttrShadow {
pub(super) fn new(rows: u16, cols: u16) -> Self {
Self {
parser: ::vt100::Parser::new(rows, cols, 0),
state: State::Ground,
pending: Vec::new(),
}
}
pub(super) fn set_size(&mut self, rows: u16, cols: u16) {
self.parser.screen_mut().set_size(rows, cols);
}
pub(super) fn cell(&self, row: u16, col: u16) -> Option<&::vt100::Cell> {
self.parser.screen().cell(row, col)
}
pub(super) fn contents(&self) -> String {
self.parser.screen().contents()
}
pub(super) fn feed(&mut self, bytes: &[u8]) {
let shadowed = self.rewrite_stream(bytes);
self.parser.process(&shadowed);
}
fn rewrite_stream(&mut self, bytes: &[u8]) -> Vec<u8> {
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
for &b in bytes {
if b == 0x1b {
out.append(&mut self.pending);
self.pending.push(b);
self.state = State::Esc;
continue;
}
match self.state {
State::Ground => out.push(b),
State::Esc => {
self.pending.push(b);
if b == b'[' {
self.state = State::Csi;
} else {
out.append(&mut self.pending);
self.state = State::Ground;
}
}
State::Csi => {
self.pending.push(b);
match b {
0x20..=0x3f => {}
0x40..=0x7e => {
let seq = std::mem::take(&mut self.pending);
self.state = State::Ground;
match (b == b'm').then(|| rewrite_sgr(&seq)).flatten() {
Some(rewritten) => out.extend_from_slice(&rewritten),
None => out.extend_from_slice(&seq),
}
}
_ => {
out.append(&mut self.pending);
self.state = State::Ground;
}
}
}
}
}
out
}
}
fn rewrite_sgr(seq: &[u8]) -> Option<Vec<u8>> {
let params = seq.get(2..seq.len().checked_sub(1)?)?;
if params
.iter()
.any(|b| !matches!(b, b'0'..=b'9' | b';' | b':'))
{
return None;
}
fn value(group: &[u8]) -> u32 {
let first = group.split(|&b| b == b':').next().unwrap_or(b"");
first.iter().fold(0u32, |acc, &d| {
acc.saturating_mul(10).saturating_add(u32::from(d - b'0'))
})
}
let groups: Vec<&[u8]> = params.split(|&b| b == b';').collect();
let mut out: Vec<u32> = Vec::new();
let mut i = 0;
while i < groups.len() {
let group = groups[i];
let v = value(group);
match v {
38 | 48 | 58 => {
if group.contains(&b':') {
i += 1;
} else {
i += match groups.get(i + 1).map(|g| value(g)) {
Some(2) => 5, Some(5) => 3, _ => 1, };
}
}
_ => {
if let Some(carrier) = carrier(v) {
out.push(carrier);
}
i += 1;
}
}
}
let mut bytes = Vec::new();
if !out.is_empty() {
bytes.extend_from_slice(b"\x1b[");
for (n, param) in out.iter().enumerate() {
if n > 0 {
bytes.push(b';');
}
bytes.extend_from_slice(param.to_string().as_bytes());
}
bytes.push(b'm');
}
Some(bytes)
}
fn carrier(param: u32) -> Option<u32> {
match param {
0 => Some(0), 5 | 6 => Some(1), 8 => Some(3), 9 => Some(4), 25 => Some(22), 28 => Some(23), 29 => Some(24), _ => None, }
}
#[cfg(test)]
mod tests {
use super::*;
fn shadowed(bytes: &[u8]) -> String {
let mut shadow = AttrShadow::new(4, 20);
let mut out = shadow.rewrite_stream(bytes);
out.append(&mut shadow.pending);
String::from_utf8_lossy(&out).replace('\x1b', "E")
}
#[test]
fn the_three_dropped_attributes_get_carriers() {
assert_eq!(shadowed(b"\x1b[5mX"), "E[1mX"); assert_eq!(shadowed(b"\x1b[6mX"), "E[1mX"); assert_eq!(shadowed(b"\x1b[8mX"), "E[3mX"); assert_eq!(shadowed(b"\x1b[9mX"), "E[4mX"); assert_eq!(shadowed(b"\x1b[25m"), "E[22m");
assert_eq!(shadowed(b"\x1b[28m"), "E[23m");
assert_eq!(shadowed(b"\x1b[29m"), "E[24m");
}
#[test]
fn the_primarys_own_attributes_are_dropped_from_the_shadow() {
assert_eq!(shadowed(b"\x1b[1mX"), "X");
assert_eq!(shadowed(b"\x1b[3mX"), "X");
assert_eq!(shadowed(b"\x1b[4mX"), "X");
assert_eq!(shadowed(b"\x1b[7;31;44mX"), "X");
assert_eq!(shadowed(b"\x1b[22m\x1b[23m\x1b[24m\x1b[27m"), "");
assert_eq!(shadowed(b"\x1b[0mX"), "E[0mX");
assert_eq!(shadowed(b"\x1b[mX"), "E[0mX");
}
#[test]
fn mixed_parameters_keep_only_the_carriers_in_order() {
assert_eq!(shadowed(b"\x1b[1;5;31mX"), "E[1mX");
assert_eq!(shadowed(b"\x1b[0;9;1;8mX"), "E[0;4;3mX");
}
#[test]
fn an_extended_colour_never_looks_like_a_carrier() {
assert_eq!(shadowed(b"\x1b[38;5;196mX"), "X");
assert_eq!(shadowed(b"\x1b[48;5;9mX"), "X");
assert_eq!(shadowed(b"\x1b[38;2;255;0;8mX"), "X"); assert_eq!(shadowed(b"\x1b[38;2;0;9;0;5mX"), "E[1mX"); assert_eq!(shadowed(b"\x1b[38:5:196mX"), "X");
assert_eq!(shadowed(b"\x1b[38:2::255:0:9mX"), "X");
assert_eq!(shadowed(b"\x1b[4:3mX"), "X"); assert_eq!(shadowed(b"\x1b[38;5;196;9mX"), "E[4mX");
}
#[test]
fn anything_that_is_not_a_plain_sgr_passes_through_verbatim() {
for seq in [
&b"\x1b[?2026h"[..],
&b"\x1b[2J"[..],
&b"\x1b[H"[..],
&b"\x1b[3;7Hhi"[..],
&b"\x1b[?25l"[..],
&b"\x1b[>4;2m"[..], &b"\x1b[4$p"[..], &b"\x1b]0;title\x07"[..],
&b"\x1b7\x1b8"[..],
&b"plain text\r\n\t"[..],
] {
assert_eq!(
shadowed(seq),
String::from_utf8_lossy(seq).replace('\x1b', "E"),
"sequence {:?} must pass through untouched",
String::from_utf8_lossy(seq)
);
}
}
#[test]
fn a_sequence_split_across_feeds_is_not_lost() {
let mut shadow = AttrShadow::new(2, 10);
shadow.feed(b"\x1b[8");
assert!(!shadow.cell(0, 0).is_some_and(::vt100::Cell::italic));
shadow.feed(b"mX");
assert!(
shadow.cell(0, 0).is_some_and(::vt100::Cell::italic),
"the conceal carrier must survive a chunk boundary"
);
}
#[test]
fn an_aborted_csi_keeps_its_bytes() {
assert_eq!(shadowed(b"\x1b[31\x18X"), "E[31\u{18}X");
assert_eq!(shadowed(b"\x1b[31\x18\x1b[9mX"), "E[31\u{18}E[4mX");
assert_eq!(shadowed(b"\x1b[31\x1b[9mX"), "E[31E[4mX");
}
}