diff --git a/src/perform.rs b/src/perform.rs index e36cfd8..8ce5e4c 100644 --- a/src/perform.rs +++ b/src/perform.rs @@ -123,6 +123,8 @@ impl vte::Perform for WrappedScreen { 'S' => self.screen.su(canonicalize_params_1(params, 1)), 'T' => self.screen.sd(canonicalize_params_1(params, 1)), 'X' => self.screen.ech(canonicalize_params_1(params, 1)), + '`' => self.screen.hpa(canonicalize_params_1(params, 1)), + 'b' => self.screen.rep(canonicalize_params_1(params, 1)), 'd' => self.screen.vpa(canonicalize_params_1(params, 1)), 'm' => self.screen.sgr(params, unhandled), 'r' => self.screen.decstbm(canonicalize_params_decstbm( diff --git a/src/screen.rs b/src/screen.rs index 7dfec97..6b08d6b 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -62,6 +62,8 @@ pub struct Screen { modes: u8, mouse_protocol_mode: MouseProtocolMode, mouse_protocol_encoding: MouseProtocolEncoding, + + last_char: Option, } impl Screen { @@ -81,6 +83,8 @@ impl Screen { modes: 0, mouse_protocol_mode: MouseProtocolMode::default(), mouse_protocol_encoding: MouseProtocolEncoding::default(), + + last_char: None, } } @@ -880,6 +884,7 @@ impl Screen { // that self.grid().pos().col has a valid value. .unwrap(); cell.set(c, attrs); + self.last_char = Some(c); self.grid_mut().col_inc(1); if width > 1 { let pos = self.grid().pos(); @@ -1043,6 +1048,20 @@ impl Screen { self.grid_mut().col_set(col - 1); } + // CSI ` (HPA - Horizontal Position Absolute, equivalent to CHA) + pub(crate) fn hpa(&mut self, col: u16) { + self.cha(col); + } + + // CSI b (REP - Repeat last printed character) + pub(crate) fn rep(&mut self, count: u16) { + if let Some(c) = self.last_char { + for _ in 0..count { + self.text(c); + } + } + } + // CSI H pub(crate) fn cup(&mut self, (row, col): (u16, u16)) { self.grid_mut().set_pos(crate::grid::Pos { diff --git a/tests/csi.rs b/tests/csi.rs index 7ebcfaa..1a3cb8a 100644 --- a/tests/csi.rs +++ b/tests/csi.rs @@ -86,3 +86,53 @@ fn xtwinops() { "bbbbbcccccccccccccccccccc" ); } + +#[test] +fn hpa() { + let mut vt = vt100::Parser::default(); + + // HPA (CSI `) is equivalent to CHA (CSI G) - Horizontal Position Absolute + vt.process(b"hello"); + assert_eq!(vt.screen().cursor_position(), (0, 5)); + + // Move to column 10 (1-indexed, so position 9) + vt.process(b"\x1b[10`"); + assert_eq!(vt.screen().cursor_position(), (0, 9)); + + // Move to column 1 (default) + vt.process(b"\x1b[`"); + assert_eq!(vt.screen().cursor_position(), (0, 0)); + + // Move to column 50 + vt.process(b"\x1b[50`"); + assert_eq!(vt.screen().cursor_position(), (0, 49)); + + // Clamp to screen width (80 columns, so max position is 79) + vt.process(b"\x1b[500`"); + assert_eq!(vt.screen().cursor_position(), (0, 79)); +} + +#[test] +fn rep() { + let mut vt = vt100::Parser::default(); + + // REP (CSI b) repeats the last printed character + vt.process(b"x\x1b[5b"); + assert_eq!(vt.screen().rows(0, 80).next().unwrap(), "xxxxxx"); + assert_eq!(vt.screen().cursor_position(), (0, 6)); + + // REP with default count of 1 + vt.process(b"\x1b[Hy\x1b[b"); + assert_eq!(vt.screen().rows(0, 80).next().unwrap(), "yyxxxx"); + assert_eq!(vt.screen().cursor_position(), (0, 2)); + + // REP on a new line + vt.process(b"\x1b[2;1Ha\x1b[3b"); + assert_eq!(vt.screen().rows(0, 80).nth(1).unwrap(), "aaaa"); + + // REP should not do anything if no character was printed yet + let mut vt2 = vt100::Parser::default(); + vt2.process(b"\x1b[5b"); + assert_eq!(vt2.screen().rows(0, 80).next().unwrap(), ""); + assert_eq!(vt2.screen().cursor_position(), (0, 0)); +}