use std::collections::VecDeque;
use unicode_width::UnicodeWidthChar;
const WIDE_CONTINUATION: char = '\0';
fn dec_graphics_char(ch: char) -> char {
match ch {
'`' => '\u{25C6}', 'a' => '\u{2592}', 'b' => '\u{2409}', 'c' => '\u{240C}', 'd' => '\u{240D}', 'e' => '\u{240A}', 'f' => '\u{00B0}', 'g' => '\u{00B1}', 'h' => '\u{2424}', 'i' => '\u{240B}', 'j' => '\u{2518}', 'k' => '\u{2510}', 'l' => '\u{250C}', 'm' => '\u{2514}', 'n' => '\u{253C}', 'o' => '\u{23BA}', 'p' => '\u{23BB}', 'q' => '\u{2500}', 'r' => '\u{23BC}', 's' => '\u{23BD}', 't' => '\u{251C}', 'u' => '\u{2524}', 'v' => '\u{2534}', 'w' => '\u{252C}', 'x' => '\u{2502}', 'y' => '\u{2264}', 'z' => '\u{2265}', '{' => '\u{03C0}', '|' => '\u{2260}', '}' => '\u{00A3}', '~' => '\u{00B7}', _ => ch,
}
}
fn translate_charset(ch: char, designator: u8) -> char {
match designator {
b'0' => dec_graphics_char(ch),
_ => ch,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Color {
#[must_use]
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CellStyle {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub blink: bool,
pub reverse: bool,
pub strikethrough: bool,
pub hidden: bool,
pub overline: bool,
}
impl CellStyle {
fn reset(&mut self) {
*self = Self::default();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VCell {
pub ch: char,
pub style: CellStyle,
}
impl Default for VCell {
fn default() -> Self {
Self {
ch: ' ',
style: CellStyle::default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParseState {
Ground,
Escape,
EscapeHash,
EscapeCharset(u8),
Csi,
Osc,
OscEscapeSeen,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalQuirk {
TmuxNestedCursorSaveRestore,
ScreenImmediateWrap,
WindowsNoAltScreen,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QuirkSet {
tmux_nested_cursor: bool,
screen_immediate_wrap: bool,
windows_no_alt_screen: bool,
}
impl Default for QuirkSet {
fn default() -> Self {
Self::empty()
}
}
impl QuirkSet {
#[must_use]
pub const fn empty() -> Self {
Self {
tmux_nested_cursor: false,
screen_immediate_wrap: false,
windows_no_alt_screen: false,
}
}
#[must_use]
pub const fn tmux_nested() -> Self {
Self {
tmux_nested_cursor: true,
..Self::empty()
}
}
#[must_use]
pub const fn gnu_screen() -> Self {
Self {
screen_immediate_wrap: true,
..Self::empty()
}
}
#[must_use]
pub const fn windows_console() -> Self {
Self {
windows_no_alt_screen: true,
..Self::empty()
}
}
#[must_use]
pub const fn with_tmux_nested_cursor(mut self, enabled: bool) -> Self {
self.tmux_nested_cursor = enabled;
self
}
#[must_use]
pub const fn with_screen_immediate_wrap(mut self, enabled: bool) -> Self {
self.screen_immediate_wrap = enabled;
self
}
#[must_use]
pub const fn with_windows_no_alt_screen(mut self, enabled: bool) -> Self {
self.windows_no_alt_screen = enabled;
self
}
#[must_use]
pub const fn has(self, quirk: TerminalQuirk) -> bool {
match quirk {
TerminalQuirk::TmuxNestedCursorSaveRestore => self.tmux_nested_cursor,
TerminalQuirk::ScreenImmediateWrap => self.screen_immediate_wrap,
TerminalQuirk::WindowsNoAltScreen => self.windows_no_alt_screen,
}
}
}
pub struct VirtualTerminal {
width: u16,
height: u16,
grid: Vec<VCell>,
cursor_x: u16,
cursor_y: u16,
cursor_visible: bool,
current_style: CellStyle,
scrollback: VecDeque<Vec<VCell>>,
max_scrollback: usize,
saved_cursor: Option<(u16, u16)>,
scroll_top: u16,
scroll_bottom: u16,
parse_state: ParseState,
csi_params: Vec<u16>,
csi_intermediate: Vec<u8>,
osc_data: Vec<u8>,
alternate_screen: bool,
alternate_grid: Option<Vec<VCell>>,
alternate_cursor: Option<(u16, u16)>,
title: String,
quirks: QuirkSet,
origin_mode: bool,
last_char: Option<char>,
utf8_buf: [u8; 4],
utf8_len: u8,
utf8_expected: u8,
tab_stops: Vec<bool>,
insert_mode: bool,
autowrap: bool,
charset_slots: [u8; 4],
active_charset: u8,
single_shift: Option<u8>,
}
impl VirtualTerminal {
#[must_use]
pub fn new(width: u16, height: u16) -> Self {
Self::with_quirks(width, height, QuirkSet::default())
}
#[must_use]
pub fn with_quirks(width: u16, height: u16, quirks: QuirkSet) -> Self {
assert!(width > 0 && height > 0, "terminal dimensions must be > 0");
let grid = vec![VCell::default(); usize::from(width) * usize::from(height)];
Self {
width,
height,
grid,
cursor_x: 0,
cursor_y: 0,
cursor_visible: true,
current_style: CellStyle::default(),
scrollback: VecDeque::new(),
max_scrollback: 1000,
saved_cursor: None,
scroll_top: 0,
scroll_bottom: height.saturating_sub(1),
parse_state: ParseState::Ground,
csi_params: Vec::new(),
csi_intermediate: Vec::new(),
osc_data: Vec::new(),
alternate_screen: false,
alternate_grid: None,
alternate_cursor: None,
title: String::new(),
quirks,
origin_mode: false,
last_char: None,
utf8_buf: [0; 4],
utf8_len: 0,
utf8_expected: 0,
tab_stops: Self::default_tab_stops(width),
insert_mode: false,
autowrap: true,
charset_slots: [b'B'; 4],
active_charset: 0,
single_shift: None,
}
}
fn default_tab_stops(width: u16) -> Vec<bool> {
(0..width).map(|c| c > 0 && c % 8 == 0).collect()
}
#[must_use]
pub const fn width(&self) -> u16 {
self.width
}
#[must_use]
pub const fn height(&self) -> u16 {
self.height
}
#[must_use]
pub const fn cursor(&self) -> (u16, u16) {
(self.cursor_x, self.cursor_y)
}
#[must_use]
pub const fn cursor_visible(&self) -> bool {
self.cursor_visible
}
#[must_use]
pub const fn is_alternate_screen(&self) -> bool {
self.alternate_screen
}
#[must_use]
pub fn title(&self) -> &str {
&self.title
}
#[must_use]
pub const fn quirks(&self) -> QuirkSet {
self.quirks
}
pub fn set_quirks(&mut self, quirks: QuirkSet) {
self.quirks = quirks;
}
#[must_use]
pub fn scrollback_len(&self) -> usize {
self.scrollback.len()
}
pub fn set_max_scrollback(&mut self, max: usize) {
self.max_scrollback = max;
while self.scrollback.len() > self.max_scrollback {
self.scrollback.pop_front();
}
}
#[must_use]
pub fn char_at(&self, x: u16, y: u16) -> Option<char> {
self.cell_at(x, y).map(|c| c.ch)
}
#[must_use]
pub fn style_at(&self, x: u16, y: u16) -> Option<&CellStyle> {
self.cell_at(x, y).map(|c| &c.style)
}
#[must_use]
pub fn cell_at(&self, x: u16, y: u16) -> Option<&VCell> {
if x < self.width && y < self.height {
Some(&self.grid[self.idx(x, y)])
} else {
None
}
}
#[must_use]
pub fn row_text(&self, y: u16) -> String {
if y >= self.height {
return String::new();
}
let start = self.idx(0, y);
let end = start + usize::from(self.width);
let s: String = self.grid[start..end]
.iter()
.filter(|c| c.ch != WIDE_CONTINUATION)
.map(|c| c.ch)
.collect();
s.trim_end().to_string()
}
#[must_use]
pub fn screen_text(&self) -> String {
(0..self.height)
.map(|y| self.row_text(y))
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
pub fn scrollback_line(&self, idx: usize) -> Option<String> {
self.scrollback.get(idx).map(|cells| {
let s: String = cells
.iter()
.filter(|c| c.ch != WIDE_CONTINUATION)
.map(|c| c.ch)
.collect();
s.trim_end().to_string()
})
}
pub fn feed(&mut self, data: &[u8]) {
for &byte in data {
self.process_byte(byte);
}
}
pub fn feed_str(&mut self, s: &str) {
self.feed(s.as_bytes());
}
pub fn put_str(&mut self, s: &str) {
for ch in s.chars() {
self.put_char(ch);
}
}
pub fn set_cursor_position(&mut self, x: u16, y: u16) {
self.cursor_x = x.min(self.width.saturating_sub(1));
self.cursor_y = y.min(self.height.saturating_sub(1));
}
pub fn clear(&mut self) {
let blank = self.styled_blank();
for cell in &mut self.grid {
*cell = blank.clone();
}
}
pub fn clear_scrollback(&mut self) {
self.scrollback.clear();
}
#[must_use]
pub fn cpr_response(&self) -> Vec<u8> {
format!("\x1b[{};{}R", self.cursor_y + 1, self.cursor_x + 1).into_bytes()
}
#[must_use]
pub fn da1_response(&self) -> Vec<u8> {
b"\x1b[?62;22c".to_vec()
}
fn idx(&self, x: u16, y: u16) -> usize {
usize::from(y) * usize::from(self.width) + usize::from(x)
}
fn process_byte(&mut self, byte: u8) {
match self.parse_state {
ParseState::Ground => self.ground(byte),
ParseState::Escape => self.escape(byte),
ParseState::EscapeHash => self.escape_hash(byte),
ParseState::EscapeCharset(slot) => self.escape_charset(slot, byte),
ParseState::Csi => self.csi(byte),
ParseState::Osc => self.osc(byte),
ParseState::OscEscapeSeen => self.osc_escape_seen(byte),
}
}
fn ground(&mut self, byte: u8) {
match byte {
0x1b => {
self.parse_state = ParseState::Escape;
}
b'\n' | b'\x0b' | b'\x0c' => {
self.linefeed();
}
b'\r' => {
self.cursor_x = 0;
}
b'\x08' => {
self.cursor_x = self.cursor_x.saturating_sub(1);
}
b'\t' => {
if self.cursor_x >= self.width {
self.cursor_x = self.width.saturating_sub(1);
} else {
let max_col = self.width.saturating_sub(1);
let mut col = self.cursor_x + 1;
while col < self.width {
if self.tab_stops[usize::from(col)] {
break;
}
col += 1;
}
self.cursor_x = col.min(max_col);
}
}
b'\x07' => {
}
b'\x0e' => {
self.active_charset = 1;
}
b'\x0f' => {
self.active_charset = 0;
}
0x20..=0x7e => {
self.put_char(byte as char);
}
0xc2..=0xdf => {
self.utf8_buf[0] = byte;
self.utf8_len = 1;
self.utf8_expected = 2;
}
0xe0..=0xef => {
self.utf8_buf[0] = byte;
self.utf8_len = 1;
self.utf8_expected = 3;
}
0xf0..=0xf4 => {
self.utf8_buf[0] = byte;
self.utf8_len = 1;
self.utf8_expected = 4;
}
0x80..=0xbf if self.utf8_len > 0 => {
let idx = usize::from(self.utf8_len);
self.utf8_buf[idx] = byte;
self.utf8_len += 1;
if self.utf8_len == self.utf8_expected {
let len = usize::from(self.utf8_len);
let mut buf = [0u8; 4];
buf[..len].copy_from_slice(&self.utf8_buf[..len]);
self.utf8_len = 0;
self.utf8_expected = 0;
if let Ok(decoded) = std::str::from_utf8(&buf[..len]) {
for ch in decoded.chars() {
self.put_char(ch);
}
}
}
}
_ => {
self.utf8_len = 0;
self.utf8_expected = 0;
}
}
}
fn escape(&mut self, byte: u8) {
match byte {
b'[' => {
self.parse_state = ParseState::Csi;
self.csi_params.clear();
self.csi_intermediate.clear();
}
b']' => {
self.parse_state = ParseState::Osc;
self.osc_data.clear();
}
b'7' => {
if !(self.quirks.tmux_nested_cursor && self.alternate_screen) {
self.saved_cursor = Some((self.cursor_x, self.cursor_y));
}
self.parse_state = ParseState::Ground;
}
b'8' => {
if !(self.quirks.tmux_nested_cursor && self.alternate_screen)
&& let Some((x, y)) = self.saved_cursor
{
self.cursor_x = x.min(self.width.saturating_sub(1));
self.cursor_y = y.min(self.height.saturating_sub(1));
}
self.parse_state = ParseState::Ground;
}
b'H' => {
let col = usize::from(self.cursor_x);
if col < self.tab_stops.len() {
self.tab_stops[col] = true;
}
self.parse_state = ParseState::Ground;
}
b'D' => {
self.linefeed();
self.parse_state = ParseState::Ground;
}
b'E' => {
self.cursor_x = 0;
self.linefeed();
self.parse_state = ParseState::Ground;
}
b'M' => {
if self.cursor_y == self.scroll_top {
self.scroll_down();
} else {
self.cursor_y = self.cursor_y.saturating_sub(1);
}
self.parse_state = ParseState::Ground;
}
b'#' => {
self.parse_state = ParseState::EscapeHash;
}
b'(' => self.parse_state = ParseState::EscapeCharset(0), b')' => self.parse_state = ParseState::EscapeCharset(1), b'*' => self.parse_state = ParseState::EscapeCharset(2), b'+' => self.parse_state = ParseState::EscapeCharset(3), b'N' => {
self.single_shift = Some(2);
self.parse_state = ParseState::Ground;
}
b'O' => {
self.single_shift = Some(3);
self.parse_state = ParseState::Ground;
}
b'n' => {
self.active_charset = 2;
self.parse_state = ParseState::Ground;
}
b'o' => {
self.active_charset = 3;
self.parse_state = ParseState::Ground;
}
b'c' => {
self.reset();
self.parse_state = ParseState::Ground;
}
_ => {
self.parse_state = ParseState::Ground;
}
}
}
fn escape_hash(&mut self, byte: u8) {
if byte == b'8' {
for cell in self.grid.iter_mut() {
*cell = VCell {
ch: 'E',
style: CellStyle::default(),
};
}
self.scroll_top = 0;
self.scroll_bottom = self.height.saturating_sub(1);
self.cursor_x = 0;
self.cursor_y = 0;
}
self.parse_state = ParseState::Ground;
}
fn escape_charset(&mut self, slot: u8, byte: u8) {
let idx = (slot as usize).min(3);
self.charset_slots[idx] = byte;
self.parse_state = ParseState::Ground;
}
fn csi(&mut self, byte: u8) {
match byte {
b'0'..=b'9' => {
let digit = u16::from(byte - b'0');
if let Some(last) = self.csi_params.last_mut() {
*last = last.saturating_mul(10).saturating_add(digit);
} else if self.csi_params.len() < 32 {
self.csi_params.push(digit);
}
}
b';' => {
if self.csi_params.len() < 32 {
if self.csi_params.is_empty() {
self.csi_params.push(0);
}
self.csi_params.push(0);
}
}
b'?' | b'>' | b'!' | b' ' => {
if self.csi_intermediate.len() < 16 {
self.csi_intermediate.push(byte);
}
}
0x40..=0x7e => {
self.dispatch_csi(byte);
self.parse_state = ParseState::Ground;
}
_ => {
self.parse_state = ParseState::Ground;
}
}
}
fn osc(&mut self, byte: u8) {
match byte {
0x07 => {
self.dispatch_osc();
self.parse_state = ParseState::Ground;
}
0x1b => {
self.parse_state = ParseState::OscEscapeSeen;
}
_ => {
if self.osc_data.len() < 4096 {
self.osc_data.push(byte);
}
}
}
}
fn osc_escape_seen(&mut self, byte: u8) {
match byte {
b'\\' => {
self.dispatch_osc();
self.parse_state = ParseState::Ground;
}
_ => {
self.dispatch_osc();
self.parse_state = ParseState::Escape;
self.escape(byte);
}
}
}
fn dispatch_csi(&mut self, final_byte: u8) {
let params = &self.csi_params;
let has_question = self.csi_intermediate.contains(&b'?');
match final_byte {
b'A' => {
let n = Self::param(params, 0, 1);
self.cursor_y = self.cursor_y.saturating_sub(n);
}
b'B' => {
let n = Self::param(params, 0, 1);
self.cursor_y = (self.cursor_y + n).min(self.height.saturating_sub(1));
}
b'C' => {
let n = Self::param(params, 0, 1);
self.cursor_x = (self.cursor_x + n).min(self.width.saturating_sub(1));
}
b'D' => {
let n = Self::param(params, 0, 1);
self.cursor_x = self.cursor_x.saturating_sub(n);
}
b'E' => {
let n = Self::param(params, 0, 1);
self.cursor_y = (self.cursor_y + n).min(self.height.saturating_sub(1));
self.cursor_x = 0;
}
b'F' => {
let n = Self::param(params, 0, 1);
self.cursor_y = self.cursor_y.saturating_sub(n);
self.cursor_x = 0;
}
b'G' => {
let col = Self::param(params, 0, 1).saturating_sub(1);
self.cursor_x = col.min(self.width.saturating_sub(1));
}
b'H' | b'f' => {
let row = Self::param(params, 0, 1).saturating_sub(1);
let col = Self::param(params, 1, 1).saturating_sub(1);
if self.origin_mode {
let abs_row = row.saturating_add(self.scroll_top);
self.cursor_y = abs_row.min(self.scroll_bottom);
} else {
self.cursor_y = row.min(self.height.saturating_sub(1));
}
self.cursor_x = col.min(self.width.saturating_sub(1));
}
b'J' => {
let mode = Self::param(params, 0, 0);
self.erase_display(mode);
}
b'K' => {
let mode = Self::param(params, 0, 0);
self.erase_line(mode);
}
b'L' => {
let n = Self::param(params, 0, 1);
if self.cursor_y >= self.scroll_top && self.cursor_y <= self.scroll_bottom {
let blank = self.styled_blank();
for _ in 0..n {
for row in (self.cursor_y + 1..=self.scroll_bottom).rev() {
let src_start = self.idx(0, row - 1);
let dst_start = self.idx(0, row);
let w = usize::from(self.width);
if src_start < dst_start {
let (left, right) = self.grid.split_at_mut(dst_start);
right[..w].clone_from_slice(&left[src_start..src_start + w]);
}
}
let row_start = self.idx(0, self.cursor_y);
for i in 0..usize::from(self.width) {
self.grid[row_start + i] = blank.clone();
}
}
}
}
b'M' => {
let n = Self::param(params, 0, 1);
if self.cursor_y >= self.scroll_top && self.cursor_y <= self.scroll_bottom {
let blank = self.styled_blank();
for _ in 0..n {
for row in self.cursor_y..self.scroll_bottom {
let src_start = self.idx(0, row + 1);
let dst_start = self.idx(0, row);
let w = usize::from(self.width);
let (left, right) = self.grid.split_at_mut(src_start);
left[dst_start..dst_start + w].clone_from_slice(&right[..w]);
}
let bottom_start = self.idx(0, self.scroll_bottom);
for i in 0..usize::from(self.width) {
self.grid[bottom_start + i] = blank.clone();
}
}
}
}
b'S' => {
let n = Self::param(params, 0, 1);
for _ in 0..n {
self.scroll_up();
}
}
b'T' => {
let n = Self::param(params, 0, 1);
for _ in 0..n {
self.scroll_down();
}
}
b'd' => {
let row = Self::param(params, 0, 1).saturating_sub(1);
if self.origin_mode {
let abs_row = row.saturating_add(self.scroll_top);
self.cursor_y = abs_row.min(self.scroll_bottom);
} else {
self.cursor_y = row.min(self.height.saturating_sub(1));
}
}
b'm' => {
self.dispatch_sgr();
}
b'n' => {
}
b'r' => {
let top = Self::param(params, 0, 1).saturating_sub(1);
let bottom = Self::param(params, 1, self.height).saturating_sub(1);
if top < bottom && bottom < self.height {
self.scroll_top = top;
self.scroll_bottom = bottom;
}
self.cursor_x = 0;
if self.origin_mode {
self.cursor_y = self.scroll_top;
} else {
self.cursor_y = 0;
}
}
b'@' => {
let n = Self::param(params, 0, 1);
let n = n.min(self.width.saturating_sub(self.cursor_x));
self.fixup_wide_erase_row(self.cursor_y, self.cursor_x, n);
let row_start = self.idx(0, self.cursor_y);
let w = usize::from(self.width);
let cx = usize::from(self.cursor_x);
let count = usize::from(n);
if count < w {
let cutoff = w - count;
if cutoff > 0
&& cutoff < w
&& self.grid[row_start + cutoff].ch == WIDE_CONTINUATION
{
self.grid[row_start + cutoff - 1] = VCell::default();
}
}
let blank = self.styled_blank();
let row = &mut self.grid[row_start..row_start + w];
row[cx..].rotate_right(count.min(w - cx));
for cell in row.iter_mut().skip(cx).take(count.min(w - cx)) {
*cell = blank.clone();
}
if cx + count < w && row[cx + count].ch == WIDE_CONTINUATION {
row[cx + count] = blank.clone();
}
}
b'P' => {
let n = Self::param(params, 0, 1);
let n = n.min(self.width.saturating_sub(self.cursor_x));
self.fixup_wide_erase_row(self.cursor_y, self.cursor_x, n);
let blank = self.styled_blank();
let row_start = self.idx(0, self.cursor_y);
let w = usize::from(self.width);
let cx = usize::from(self.cursor_x);
let count = usize::from(n);
let row = &mut self.grid[row_start..row_start + w];
row[cx..].rotate_left(count.min(w - cx));
for cell in row.iter_mut().skip(w - count.min(w - cx)) {
*cell = blank.clone();
}
}
b'X' => {
let n = Self::param(params, 0, 1);
let n = n.min(self.width.saturating_sub(self.cursor_x));
self.fixup_wide_erase_row(self.cursor_y, self.cursor_x, n);
let blank = self.styled_blank();
let start = self.idx(self.cursor_x, self.cursor_y);
for i in 0..usize::from(n) {
self.grid[start + i] = blank.clone();
}
}
b'b' => {
let n = Self::param(params, 0, 1);
if let Some(ch) = self.last_char {
for _ in 0..n {
self.put_char(ch);
}
}
}
b'Z' => {
let n = Self::param(params, 0, 1);
for _ in 0..n {
if self.cursor_x == 0 {
break;
}
let mut col = self.cursor_x - 1;
loop {
if self.tab_stops[usize::from(col)] {
break;
}
if col == 0 {
break;
}
col -= 1;
}
self.cursor_x = col;
}
}
b'g' => {
let mode = Self::param(params, 0, 0);
match mode {
0 => {
let col = usize::from(self.cursor_x);
if col < self.tab_stops.len() {
self.tab_stops[col] = false;
}
}
3 | 5 => {
self.tab_stops.fill(false);
}
_ => {}
}
}
b'p' if self.csi_intermediate.contains(&b'!') => {
self.current_style = CellStyle::default();
self.cursor_visible = true;
self.origin_mode = false;
self.scroll_top = 0;
self.scroll_bottom = self.height.saturating_sub(1);
self.insert_mode = false;
self.autowrap = true;
self.charset_slots = [b'B'; 4];
self.active_charset = 0;
self.single_shift = None;
}
b'h' if has_question => {
let modes: Vec<u16> = self.csi_params.clone();
for p in modes {
self.set_dec_mode(p, true);
}
}
b'l' if has_question => {
let modes: Vec<u16> = self.csi_params.clone();
for p in modes {
self.set_dec_mode(p, false);
}
}
b'h' if !has_question => {
let modes: Vec<u16> = self.csi_params.clone();
for p in modes {
self.set_ansi_mode(p, true);
}
}
b'l' if !has_question => {
let modes: Vec<u16> = self.csi_params.clone();
for p in modes {
self.set_ansi_mode(p, false);
}
}
_ => {
}
}
}
fn dispatch_sgr(&mut self) {
if self.csi_params.is_empty() {
self.current_style.reset();
return;
}
let params = self.csi_params.clone();
let mut i = 0;
while i < params.len() {
match params[i] {
0 => self.current_style.reset(),
1 => self.current_style.bold = true,
2 => self.current_style.dim = true,
3 => self.current_style.italic = true,
4 => self.current_style.underline = true,
5 => self.current_style.blink = true,
7 => self.current_style.reverse = true,
8 => self.current_style.hidden = true,
9 => self.current_style.strikethrough = true,
22 => {
self.current_style.bold = false;
self.current_style.dim = false;
}
23 => self.current_style.italic = false,
24 => self.current_style.underline = false,
25 => self.current_style.blink = false,
27 => self.current_style.reverse = false,
28 => self.current_style.hidden = false,
29 => self.current_style.strikethrough = false,
53 => self.current_style.overline = true,
55 => self.current_style.overline = false,
30..=37 => {
self.current_style.fg = Some(ansi_color(params[i] - 30));
}
38 => {
if let Some(color) = parse_extended_color(¶ms, &mut i) {
self.current_style.fg = Some(color);
}
}
39 => self.current_style.fg = None,
40..=47 => {
self.current_style.bg = Some(ansi_color(params[i] - 40));
}
48 => {
if let Some(color) = parse_extended_color(¶ms, &mut i) {
self.current_style.bg = Some(color);
}
}
49 => self.current_style.bg = None,
90..=97 => {
self.current_style.fg = Some(ansi_bright_color(params[i] - 90));
}
100..=107 => {
self.current_style.bg = Some(ansi_bright_color(params[i] - 100));
}
_ => {} }
i += 1;
}
}
fn dispatch_osc(&mut self) {
let data = String::from_utf8_lossy(&self.osc_data).to_string();
if let Some(rest) = data.strip_prefix("0;").or_else(|| data.strip_prefix("2;")) {
self.title = rest.to_string();
}
}
fn set_dec_mode(&mut self, mode: u16, enable: bool) {
match mode {
6 => {
self.origin_mode = enable;
if enable {
self.cursor_x = 0;
self.cursor_y = self.scroll_top;
} else {
self.cursor_x = 0;
self.cursor_y = 0;
}
}
7 => self.autowrap = enable,
25 => self.cursor_visible = enable,
1049 => {
if self.quirks.windows_no_alt_screen {
return;
}
if enable && !self.alternate_screen {
self.alternate_grid = Some(std::mem::replace(
&mut self.grid,
vec![VCell::default(); usize::from(self.width) * usize::from(self.height)],
));
self.alternate_cursor = Some((self.cursor_x, self.cursor_y));
self.cursor_x = 0;
self.cursor_y = 0;
self.alternate_screen = true;
} else if !enable && self.alternate_screen {
if let Some(main_grid) = self.alternate_grid.take() {
self.grid = main_grid;
}
if let Some((x, y)) = self.alternate_cursor.take() {
self.cursor_x = x;
self.cursor_y = y;
}
self.alternate_screen = false;
}
}
1047 => {
if self.quirks.windows_no_alt_screen {
return;
}
if enable && !self.alternate_screen {
self.alternate_grid = Some(std::mem::replace(
&mut self.grid,
vec![VCell::default(); usize::from(self.width) * usize::from(self.height)],
));
self.alternate_screen = true;
} else if !enable && self.alternate_screen {
if let Some(main_grid) = self.alternate_grid.take() {
self.grid = main_grid;
}
self.alternate_screen = false;
}
}
_ => {
}
}
}
fn set_ansi_mode(&mut self, mode: u16, enable: bool) {
if mode == 4 {
self.insert_mode = enable;
}
}
pub fn put_char(&mut self, ch: char) {
let designator = if let Some(shift) = self.single_shift {
let slot = (shift as usize).min(3);
self.single_shift = None;
self.charset_slots[slot]
} else {
self.charset_slots[(self.active_charset as usize) & 3]
};
let ch = translate_charset(ch, designator);
let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
if char_width == 0 {
return; }
if self.cursor_x >= self.width {
if self.autowrap {
self.cursor_x = 0;
self.linefeed();
} else {
self.cursor_x = self.width.saturating_sub(1);
}
}
if char_width == 2 && self.cursor_x + 1 >= self.width {
if self.autowrap {
let idx = self.idx(self.cursor_x, self.cursor_y);
self.grid[idx] = VCell::default();
self.cursor_x = 0;
self.linefeed();
} else {
self.cursor_x = self.width.saturating_sub(1);
}
}
let last_col = self.width.saturating_sub(1);
let immediate_wrap = self.quirks.screen_immediate_wrap && self.cursor_x == last_col;
let idx = self.idx(self.cursor_x, self.cursor_y);
if self.insert_mode {
let row_start = self.idx(0, self.cursor_y);
let w = usize::from(self.width);
let cx = usize::from(self.cursor_x);
let shift = usize::from(u16::try_from(char_width).unwrap_or(1));
let row = &mut self.grid[row_start..row_start + w];
if cx + shift <= w {
row[cx..].rotate_right(shift.min(w - cx));
}
}
if self.grid[idx].ch == WIDE_CONTINUATION && self.cursor_x > 0 {
let lead_idx = self.idx(self.cursor_x - 1, self.cursor_y);
self.grid[lead_idx] = VCell::default();
}
if char_width == 1 && self.cursor_x + 1 < self.width {
let next_idx = idx + 1;
if self.grid[next_idx].ch == WIDE_CONTINUATION {
self.grid[next_idx] = VCell::default();
}
}
self.grid[idx] = VCell {
ch,
style: self.current_style.clone(),
};
if char_width == 2 && self.cursor_x + 1 < self.width {
let cont_idx = idx + 1;
self.grid[cont_idx] = VCell {
ch: WIDE_CONTINUATION,
style: self.current_style.clone(),
};
}
self.last_char = Some(ch);
let advance = u16::try_from(char_width).unwrap_or(1);
if immediate_wrap {
self.cursor_x = 0;
self.linefeed();
} else if self.autowrap {
self.cursor_x += advance;
} else {
self.cursor_x = (self.cursor_x + advance).min(self.width.saturating_sub(1));
}
}
fn linefeed(&mut self) {
if self.cursor_y == self.scroll_bottom {
self.scroll_up();
} else if self.cursor_y < self.height.saturating_sub(1) {
self.cursor_y += 1;
}
}
fn scroll_up(&mut self) {
let top_start = self.idx(0, self.scroll_top);
let top_end = top_start + usize::from(self.width);
let line: Vec<VCell> = self.grid[top_start..top_end].to_vec();
self.scrollback.push_back(line);
while self.scrollback.len() > self.max_scrollback {
self.scrollback.pop_front();
}
for row in self.scroll_top..self.scroll_bottom {
let src_start = self.idx(0, row + 1);
let dst_start = self.idx(0, row);
let w = usize::from(self.width);
let (left, right) = self.grid.split_at_mut(src_start);
left[dst_start..dst_start + w].clone_from_slice(&right[..w]);
}
let blank = self.styled_blank();
let bottom_start = self.idx(0, self.scroll_bottom);
for i in 0..usize::from(self.width) {
self.grid[bottom_start + i] = blank.clone();
}
}
fn scroll_down(&mut self) {
for row in (self.scroll_top + 1..=self.scroll_bottom).rev() {
let src_start = self.idx(0, row - 1);
let dst_start = self.idx(0, row);
let w = usize::from(self.width);
if src_start < dst_start {
let (left, right) = self.grid.split_at_mut(dst_start);
right[..w].clone_from_slice(&left[src_start..src_start + w]);
}
}
let blank = self.styled_blank();
let top_start = self.idx(0, self.scroll_top);
for i in 0..usize::from(self.width) {
self.grid[top_start + i] = blank.clone();
}
}
fn styled_blank(&self) -> VCell {
VCell {
ch: ' ',
style: self.current_style.clone(),
}
}
fn fixup_wide_erase_row(&mut self, row_y: u16, start_col: u16, count: u16) {
let w = self.width;
let sc = start_col;
let n = count;
if n == 0 || sc >= w {
return;
}
let row_start = self.idx(0, row_y);
if sc > 0 && self.grid[row_start + usize::from(sc)].ch == WIDE_CONTINUATION {
self.grid[row_start + usize::from(sc - 1)] = VCell::default();
}
let end_col = sc.saturating_add(n);
if end_col < w && self.grid[row_start + usize::from(end_col)].ch == WIDE_CONTINUATION {
self.grid[row_start + usize::from(end_col)] = VCell::default();
}
}
fn erase_display(&mut self, mode: u16) {
let blank = self.styled_blank();
match mode {
0 => {
let count = self.width.saturating_sub(self.cursor_x);
self.fixup_wide_erase_row(self.cursor_y, self.cursor_x, count);
let start = self.idx(self.cursor_x, self.cursor_y);
for cell in &mut self.grid[start..] {
*cell = blank.clone();
}
}
1 => {
let count = self.cursor_x + 1;
self.fixup_wide_erase_row(self.cursor_y, 0, count);
let end = self.idx(self.cursor_x, self.cursor_y) + 1;
for cell in &mut self.grid[..end] {
*cell = blank.clone();
}
}
2 | 3 => {
for cell in &mut self.grid {
*cell = blank.clone();
}
if mode == 3 {
self.scrollback.clear();
}
}
_ => {}
}
}
fn erase_line(&mut self, mode: u16) {
let y = self.cursor_y;
let blank = self.styled_blank();
let row_start = self.idx(0, y);
match mode {
0 => {
let count = self.width.saturating_sub(self.cursor_x);
self.fixup_wide_erase_row(y, self.cursor_x, count);
let start = row_start + usize::from(self.cursor_x);
let end = row_start + usize::from(self.width);
for cell in &mut self.grid[start..end] {
*cell = blank.clone();
}
}
1 => {
let count = self.cursor_x + 1;
self.fixup_wide_erase_row(y, 0, count);
let end = row_start + usize::from(count);
for cell in &mut self.grid[row_start..end] {
*cell = blank.clone();
}
}
2 => {
let end = row_start + usize::from(self.width);
for cell in &mut self.grid[row_start..end] {
*cell = blank.clone();
}
}
_ => {}
}
}
fn reset(&mut self) {
self.grid = vec![VCell::default(); usize::from(self.width) * usize::from(self.height)];
self.cursor_x = 0;
self.cursor_y = 0;
self.cursor_visible = true;
self.current_style = CellStyle::default();
self.scrollback.clear();
self.saved_cursor = None;
self.scroll_top = 0;
self.scroll_bottom = self.height.saturating_sub(1);
self.title.clear();
self.alternate_screen = false;
self.alternate_grid = None;
self.alternate_cursor = None;
self.last_char = None;
self.utf8_len = 0;
self.utf8_expected = 0;
self.tab_stops = Self::default_tab_stops(self.width);
self.insert_mode = false;
self.autowrap = true;
self.charset_slots = [b'B'; 4];
self.active_charset = 0;
self.single_shift = None;
}
fn param(params: &[u16], idx: usize, default: u16) -> u16 {
params
.get(idx)
.copied()
.filter(|&v| v > 0)
.unwrap_or(default)
}
}
fn ansi_color(idx: u16) -> Color {
match idx {
0 => Color::new(0, 0, 0), 1 => Color::new(170, 0, 0), 2 => Color::new(0, 170, 0), 3 => Color::new(170, 170, 0), 4 => Color::new(0, 0, 170), 5 => Color::new(170, 0, 170), 6 => Color::new(0, 170, 170), 7 => Color::new(170, 170, 170), _ => Color::default(),
}
}
fn ansi_bright_color(idx: u16) -> Color {
match idx {
0 => Color::new(85, 85, 85), 1 => Color::new(255, 85, 85), 2 => Color::new(85, 255, 85), 3 => Color::new(255, 255, 85), 4 => Color::new(85, 85, 255), 5 => Color::new(255, 85, 255), 6 => Color::new(85, 255, 255), 7 => Color::new(255, 255, 255), _ => Color::default(),
}
}
fn parse_extended_color(params: &[u16], i: &mut usize) -> Option<Color> {
if *i + 1 >= params.len() {
return None;
}
match params[*i + 1] {
2 => {
if *i + 4 < params.len() {
let r = params[*i + 2] as u8;
let g = params[*i + 3] as u8;
let b = params[*i + 4] as u8;
*i += 4;
Some(Color::new(r, g, b))
} else {
None
}
}
5 => {
if *i + 2 < params.len() {
let idx = params[*i + 2];
*i += 2;
Some(color_256(idx))
} else {
None
}
}
_ => None,
}
}
fn color_256(idx: u16) -> Color {
match idx {
0..=7 => ansi_color(idx),
8..=15 => ansi_bright_color(idx - 8),
16..=231 => {
let n = idx - 16;
let b = (n % 6) as u8;
let g = ((n / 6) % 6) as u8;
let r = (n / 36) as u8;
let to_rgb = |v: u8| if v == 0 { 0u8 } else { 55 + 40 * v };
Color::new(to_rgb(r), to_rgb(g), to_rgb(b))
}
232..=255 => {
let v = (8 + 10 * (idx - 232)) as u8;
Color::new(v, v, v)
}
_ => Color::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_invariants(vt: &VirtualTerminal) {
assert!(vt.cursor_x <= vt.width);
assert!(vt.cursor_y < vt.height);
assert_eq!(vt.grid.len(), vt.width as usize * vt.height as usize);
assert!(vt.scroll_top <= vt.scroll_bottom);
assert!(vt.scroll_bottom < vt.height);
for line in &vt.scrollback {
assert_eq!(line.len(), vt.width as usize);
}
}
#[test]
fn new_terminal_dimensions() {
let vt = VirtualTerminal::new(80, 24);
assert_eq!(vt.width(), 80);
assert_eq!(vt.height(), 24);
assert_eq!(vt.cursor(), (0, 0));
assert!(vt.cursor_visible());
}
#[test]
#[should_panic(expected = "dimensions must be > 0")]
fn zero_width_panics() {
let _ = VirtualTerminal::new(0, 24);
}
#[test]
#[should_panic(expected = "dimensions must be > 0")]
fn zero_height_panics() {
let _ = VirtualTerminal::new(80, 0);
}
#[test]
fn invariants_hold_for_varied_inputs() {
let inputs: [&[u8]; 6] = [
b"",
b"Hello",
b"ABCDE\r\nFGHIJ",
b"\x1b[2J",
b"\x1b[1;1H\x1b[2;2H",
b"\x1b[?1049hAlt\x1b[?1049l",
];
for width in 1..=6 {
for height in 1..=4 {
for input in inputs {
let mut vt = VirtualTerminal::new(width, height);
for chunk in input.chunks(3) {
vt.feed(chunk);
assert_invariants(&vt);
}
}
}
}
}
#[test]
fn plain_text_output() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"Hello, World!");
assert_eq!(vt.char_at(0, 0), Some('H'));
assert_eq!(vt.char_at(12, 0), Some('!'));
assert_eq!(vt.cursor(), (13, 0));
assert_eq!(vt.row_text(0), "Hello, World!");
}
#[test]
fn newline_advances_cursor() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"Line 1\r\nLine 2");
assert_eq!(vt.row_text(0), "Line 1");
assert_eq!(vt.row_text(1), "Line 2");
assert_eq!(vt.cursor(), (6, 1));
}
#[test]
fn carriage_return() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"AAAA\rBB");
assert_eq!(vt.row_text(0), "BBAA");
}
#[test]
fn auto_wrap() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"ABCDEFGH");
assert_eq!(vt.row_text(0), "ABCDE");
assert_eq!(vt.row_text(1), "FGH");
assert_eq!(vt.cursor(), (3, 1));
}
#[test]
fn screen_immediate_wrap_quirk_wraps_on_last_column() {
let mut vt = VirtualTerminal::with_quirks(5, 3, QuirkSet::gnu_screen());
vt.feed(b"ABCDE");
assert_eq!(vt.row_text(0), "ABCDE");
assert_eq!(vt.cursor(), (0, 1));
vt.feed(b"F");
assert_eq!(vt.row_text(1), "F");
assert_eq!(vt.cursor(), (1, 1));
}
#[test]
fn scroll_on_overflow() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"AAA\r\nBBB\r\nCCC\r\nDDD");
assert_eq!(vt.row_text(0), "BBB");
assert_eq!(vt.row_text(1), "CCC");
assert_eq!(vt.row_text(2), "DDD");
assert_eq!(vt.scrollback_len(), 1);
assert_eq!(vt.scrollback_line(0), Some("AAA".to_string()));
}
#[test]
fn cursor_movement_csi() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b[4;6H");
assert_eq!(vt.cursor(), (5, 3));
}
#[test]
fn cursor_up_down_forward_back() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b[10;10H"); vt.feed(b"\x1b[3A"); assert_eq!(vt.cursor(), (9, 6));
vt.feed(b"\x1b[2B"); assert_eq!(vt.cursor(), (9, 8));
vt.feed(b"\x1b[5C"); assert_eq!(vt.cursor(), (14, 8));
vt.feed(b"\x1b[3D"); assert_eq!(vt.cursor(), (11, 8));
}
#[test]
fn cursor_clamps_to_bounds() {
let mut vt = VirtualTerminal::new(10, 5);
vt.feed(b"\x1b[100;100H");
assert_eq!(vt.cursor(), (9, 4));
vt.feed(b"\x1b[99A");
assert_eq!(vt.cursor(), (9, 0));
}
#[test]
fn erase_to_end_of_line() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[1;6H"); vt.feed(b"\x1b[K"); assert_eq!(vt.row_text(0), "ABCDE");
}
#[test]
fn erase_entire_line() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[2K"); assert_eq!(vt.row_text(0), "");
}
#[test]
fn erase_display_from_cursor() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"AAAAAAAAAA");
vt.feed(b"BBBBBBBBBB");
vt.feed(b"CCCCCCCCCC");
vt.feed(b"\x1b[2;5H"); vt.feed(b"\x1b[J"); assert_eq!(vt.row_text(0), "AAAAAAAAAA");
assert_eq!(vt.row_text(1), "BBBB");
assert_eq!(vt.row_text(2), "");
}
#[test]
fn sgr_bold_and_color() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b[1;31mHello\x1b[0m World");
let style = vt.style_at(0, 0).unwrap();
assert!(style.bold);
assert_eq!(style.fg, Some(Color::new(170, 0, 0)));
let style2 = vt.style_at(6, 0).unwrap();
assert!(!style2.bold);
assert_eq!(style2.fg, None);
}
#[test]
fn sgr_truecolor() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b[38;2;100;200;50mX");
let style = vt.style_at(0, 0).unwrap();
assert_eq!(style.fg, Some(Color::new(100, 200, 50)));
}
#[test]
fn sgr_256_color() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b[48;5;196mX"); let style = vt.style_at(0, 0).unwrap();
assert!(style.bg.is_some());
}
#[test]
fn dec_save_restore_cursor() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b[5;10H"); vt.feed(b"\x1b7"); vt.feed(b"\x1b[1;1H"); assert_eq!(vt.cursor(), (0, 0));
vt.feed(b"\x1b8"); assert_eq!(vt.cursor(), (9, 4));
}
#[test]
fn tmux_nested_cursor_quirk_ignores_save_restore_in_alt_screen() {
let mut vt = VirtualTerminal::with_quirks(80, 24, QuirkSet::tmux_nested());
vt.feed(b"\x1b[?1049h"); vt.feed(b"\x1b[5;10H"); vt.feed(b"\x1b7"); vt.feed(b"\x1b[1;1H"); vt.feed(b"\x1b8"); assert_eq!(vt.cursor(), (0, 0));
}
#[test]
fn combined_quirks_apply_independently() {
let quirks = QuirkSet::empty()
.with_screen_immediate_wrap(true)
.with_tmux_nested_cursor(true);
let mut vt = VirtualTerminal::with_quirks(5, 3, quirks);
vt.feed(b"\x1b[?1049h");
vt.feed(b"ABCDE");
assert_eq!(vt.cursor(), (0, 1));
vt.feed(b"\x1b[2;2H\x1b7\x1b[1;1H\x1b8");
assert_eq!(vt.cursor(), (0, 0));
}
#[test]
fn cursor_visibility() {
let mut vt = VirtualTerminal::new(80, 24);
assert!(vt.cursor_visible());
vt.feed(b"\x1b[?25l"); assert!(!vt.cursor_visible());
vt.feed(b"\x1b[?25h"); assert!(vt.cursor_visible());
}
#[test]
fn alternate_screen() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"Main");
assert_eq!(vt.row_text(0), "Main");
assert!(!vt.is_alternate_screen());
vt.feed(b"\x1b[?1049h"); assert!(vt.is_alternate_screen());
assert_eq!(vt.row_text(0), ""); vt.feed(b"Alt");
assert_eq!(vt.row_text(0), "Alt");
vt.feed(b"\x1b[?1049l"); assert!(!vt.is_alternate_screen());
assert_eq!(vt.row_text(0), "Main"); }
#[test]
fn windows_no_alt_screen_quirk_ignores_alternate_buffer() {
let mut vt = VirtualTerminal::with_quirks(10, 3, QuirkSet::windows_console());
vt.feed(b"Main");
vt.feed(b"\x1b[?1049h"); vt.feed(b"Alt");
vt.feed(b"\x1b[?1049l"); assert!(!vt.is_alternate_screen());
assert_eq!(vt.row_text(0), "MainAlt");
}
#[test]
fn osc_title() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b]0;My Title\x07");
assert_eq!(vt.title(), "My Title");
}
#[test]
fn full_reset() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"Some text\x1b[1;31m");
vt.feed(b"\x1bc"); assert_eq!(vt.cursor(), (0, 0));
assert_eq!(vt.row_text(0), "");
assert!(vt.cursor_visible());
}
#[test]
fn cpr_response_format() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b[5;10H");
let response = vt.cpr_response();
assert_eq!(response, b"\x1b[5;10R");
}
#[test]
fn da1_response() {
let vt = VirtualTerminal::new(80, 24);
let response = vt.da1_response();
assert_eq!(response, b"\x1b[?62;22c");
}
#[test]
fn scroll_region() {
let mut vt = VirtualTerminal::new(10, 5);
vt.feed(b"\x1b[2;4r");
vt.feed(b"\x1b[1;1HROW1");
vt.feed(b"\x1b[2;1HROW2");
vt.feed(b"\x1b[3;1HROW3");
vt.feed(b"\x1b[4;1HROW4");
vt.feed(b"\x1b[5;1HROW5");
assert_eq!(vt.row_text(0), "ROW1");
assert_eq!(vt.row_text(4), "ROW5");
}
#[test]
fn tab_advances_to_stop() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"AB\tC");
assert_eq!(vt.char_at(8, 0), Some('C'));
}
#[test]
fn backspace() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"ABC\x08X");
assert_eq!(vt.row_text(0), "ABX");
}
#[test]
fn screen_text() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"AAA\r\nBBB\r\nCCC");
let text = vt.screen_text();
assert_eq!(text, "AAA\nBBB\nCCC");
}
#[test]
fn scrollback_truncation() {
let mut vt = VirtualTerminal::new(10, 2);
vt.set_max_scrollback(3);
for i in 0..5 {
vt.feed_str(&format!("Line{i}\n"));
}
assert!(vt.scrollback_len() <= 3);
}
#[test]
fn out_of_bounds_cell_returns_none() {
let vt = VirtualTerminal::new(10, 5);
assert_eq!(vt.char_at(10, 0), None);
assert_eq!(vt.char_at(0, 5), None);
assert!(vt.style_at(99, 99).is_none());
}
#[test]
fn reverse_index_at_scroll_top() {
let mut vt = VirtualTerminal::new(10, 5);
vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1bM"); assert_eq!(vt.cursor(), (0, 1));
}
#[test]
fn cursor_horizontal_absolute() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b[10G");
assert_eq!(vt.cursor(), (9, 0));
}
#[test]
fn vertical_position_absolute() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b[5d");
assert_eq!(vt.cursor(), (0, 4));
}
#[test]
fn cursor_next_previous_line() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b[5;10H"); vt.feed(b"\x1b[2E"); assert_eq!(vt.cursor(), (0, 6));
vt.feed(b"\x1b[1F"); assert_eq!(vt.cursor(), (0, 5));
}
#[test]
fn bright_colors() {
let mut vt = VirtualTerminal::new(80, 24);
vt.feed(b"\x1b[91mX"); let style = vt.style_at(0, 0).unwrap();
assert_eq!(style.fg, Some(Color::new(255, 85, 85)));
}
#[test]
fn nel_next_line() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"ABCDE\x1bEX");
assert_eq!(vt.row_text(0), "ABCDE");
assert_eq!(vt.row_text(1), "X");
assert_eq!(vt.cursor(), (1, 1));
}
#[test]
fn nel_at_bottom_scrolls() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC");
vt.feed(b"\x1b[3;3H\x1bE"); assert_eq!(vt.row_text(0), "BBBBB");
assert_eq!(vt.row_text(1), "CCCCC");
assert_eq!(vt.row_text(2), "");
assert_eq!(vt.cursor(), (0, 2));
}
#[test]
fn decaln_fills_with_e() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"ABC\x1b#8");
assert_eq!(vt.row_text(0), "EEEEE");
assert_eq!(vt.row_text(1), "EEEEE");
assert_eq!(vt.row_text(2), "EEEEE");
assert_eq!(vt.cursor(), (0, 0));
}
#[test]
fn decaln_resets_scroll_region() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"\x1b[2;3r"); vt.feed(b"\x1b#8"); vt.feed(b"\x1b[3;1HZZZZZ\n"); assert_eq!(vt.row_text(0), "EEEEE");
assert_eq!(vt.row_text(1), "ZZZZZ");
assert_eq!(vt.row_text(2), "");
}
#[test]
fn utf8_basic_multibyte() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed("Aé B".as_bytes());
assert_eq!(vt.row_text(0), "Aé B");
assert_eq!(vt.cursor(), (4, 0));
}
#[test]
fn wide_char_basic() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed("A中B".as_bytes());
assert_eq!(vt.row_text(0), "A中B");
assert_eq!(vt.cursor(), (4, 0)); }
#[test]
fn wide_char_wraps_at_last_column() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed("ABCD中".as_bytes());
assert_eq!(vt.row_text(0), "ABCD");
assert_eq!(vt.row_text(1), "中");
assert_eq!(vt.cursor(), (2, 1));
}
#[test]
fn narrow_overwrites_wide_lead() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed("中".as_bytes()); vt.feed(b"\x1b[1;1HX"); assert_eq!(vt.row_text(0), "X");
assert_eq!(vt.cursor(), (1, 0));
}
#[test]
fn narrow_overwrites_wide_continuation() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed("中".as_bytes()); vt.feed(b"\x1b[1;2HX"); assert_eq!(vt.row_text(0), " X");
assert_eq!(vt.cursor(), (2, 0));
}
#[test]
fn default_tab_stops_every_8() {
let vt = VirtualTerminal::new(20, 3);
assert!(!vt.tab_stops[0]);
assert!(vt.tab_stops[8]);
assert!(vt.tab_stops[16]);
assert!(!vt.tab_stops[1]);
assert!(!vt.tab_stops[7]);
}
#[test]
fn hts_sets_custom_tab_stop() {
let mut vt = VirtualTerminal::new(20, 3);
vt.feed(b"\x1b[1;6H\x1bH");
assert!(vt.tab_stops[5]);
vt.feed(b"\x1b[1;1H\t");
assert_eq!(vt.cursor(), (5, 0));
}
#[test]
fn tbc_clears_single_tab_stop() {
let mut vt = VirtualTerminal::new(20, 3);
vt.feed(b"\x1b[1;9H\x1b[0g");
assert!(!vt.tab_stops[8]);
vt.feed(b"\x1b[1;1H\t");
assert_eq!(vt.cursor(), (16, 0));
}
#[test]
fn tbc_clears_all_tab_stops() {
let mut vt = VirtualTerminal::new(20, 3);
vt.feed(b"\x1b[3g");
vt.feed(b"\x1b[1;1H\t");
assert_eq!(vt.cursor(), (19, 0));
}
#[test]
fn cbt_moves_to_previous_tab_stop() {
let mut vt = VirtualTerminal::new(20, 3);
vt.feed(b"\x1b[1;11H\x1b[Z");
assert_eq!(vt.cursor(), (8, 0));
}
#[test]
fn cbt_at_col_zero() {
let mut vt = VirtualTerminal::new(20, 3);
vt.feed(b"\x1b[Z");
assert_eq!(vt.cursor(), (0, 0));
}
#[test]
fn reset_restores_default_tab_stops() {
let mut vt = VirtualTerminal::new(20, 3);
vt.feed(b"\x1b[3g");
assert!(!vt.tab_stops[8]);
vt.feed(b"\x1bc"); assert!(vt.tab_stops[8]);
}
#[test]
fn irm_insert_mode_shifts_right() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[4h\x1b[1;3HXY");
assert_eq!(vt.row_text(0), "ABXYCDE");
}
#[test]
fn irm_replace_mode_default() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[1;3HXY");
assert_eq!(vt.row_text(0), "ABXYE");
}
#[test]
fn irm_disable_returns_to_replace() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[4h\x1b[4l\x1b[1;3HXY");
assert_eq!(vt.row_text(0), "ABXYE");
}
#[test]
fn irm_insert_pushes_off_right_edge() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[4h\x1b[1;1HX");
assert_eq!(vt.row_text(0), "XABCD");
}
#[test]
fn decawm_enabled_wraps_at_edge() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"ABCDEF");
assert_eq!(vt.row_text(0), "ABCDE");
assert_eq!(vt.row_text(1), "F");
}
#[test]
fn decawm_disabled_no_wrap() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"\x1b[?7l");
vt.feed(b"ABCDEFGH");
assert_eq!(vt.row_text(0), "ABCDH");
assert_eq!(vt.row_text(1), "");
assert_eq!(vt.cursor(), (4, 0));
}
#[test]
fn decawm_reenable_wraps_again() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"\x1b[?7l\x1b[?7h");
vt.feed(b"ABCDEF");
assert_eq!(vt.row_text(0), "ABCDE");
assert_eq!(vt.row_text(1), "F");
}
#[test]
fn dec_graphics_g0_designation() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b(0qqxx\x1b(B");
assert_eq!(vt.char_at(0, 0).unwrap(), '\u{2500}'); assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2500}'); assert_eq!(vt.char_at(2, 0).unwrap(), '\u{2502}'); assert_eq!(vt.char_at(3, 0).unwrap(), '\u{2502}'); }
#[test]
fn dec_graphics_g1_with_so_si() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b)0\x0el\x0fl");
assert_eq!(vt.char_at(0, 0).unwrap(), '\u{250C}'); assert_eq!(vt.char_at(1, 0).unwrap(), 'l');
}
#[test]
fn dec_graphics_box_chars() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b(0lkjmn\x1b(B");
assert_eq!(vt.char_at(0, 0).unwrap(), '\u{250C}'); assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2510}'); assert_eq!(vt.char_at(2, 0).unwrap(), '\u{2518}'); assert_eq!(vt.char_at(3, 0).unwrap(), '\u{2514}'); assert_eq!(vt.char_at(4, 0).unwrap(), '\u{253C}'); }
#[test]
fn charset_reset_restores_ascii() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b(0");
vt.feed(b"\x1bc"); vt.feed(b"q");
assert_eq!(vt.char_at(0, 0).unwrap(), 'q'); }
#[test]
fn charset_soft_reset_restores_ascii() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b(0");
vt.feed(b"\x1b[!p"); vt.feed(b"q");
assert_eq!(vt.char_at(0, 0).unwrap(), 'q'); }
#[test]
fn so_si_toggle_charset() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b)0");
vt.feed(b"A"); vt.feed(b"\x0e"); vt.feed(b"q"); vt.feed(b"\x0f"); vt.feed(b"B"); assert_eq!(vt.char_at(0, 0).unwrap(), 'A');
assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2500}'); assert_eq!(vt.char_at(2, 0).unwrap(), 'B');
}
#[test]
fn ascii_passthrough_in_dec_graphics() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b(0ABC\x1b(B");
assert_eq!(vt.char_at(0, 0).unwrap(), 'A');
assert_eq!(vt.char_at(1, 0).unwrap(), 'B');
assert_eq!(vt.char_at(2, 0).unwrap(), 'C');
}
#[test]
fn ich_basic_insert() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[2@"); assert_eq!(vt.row_text(0), "AB CDE");
assert_eq!(vt.cursor(), (2, 0));
assert_invariants(&vt);
}
#[test]
fn ich_pushes_off_right_edge() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[2@"); assert_eq!(vt.row_text(0), "A BC");
assert_invariants(&vt);
}
#[test]
fn ich_at_wide_char_continuation() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed("A中B".as_bytes()); vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[2@"); assert_eq!(vt.row_text(0), "A B");
assert_invariants(&vt);
}
#[test]
fn dch_basic_delete() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[2P"); assert_eq!(vt.row_text(0), "ADE");
assert_eq!(vt.cursor(), (1, 0));
assert_invariants(&vt);
}
#[test]
fn dch_fills_blanks_at_end() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[1;1H"); vt.feed(b"\x1b[3P"); assert_eq!(vt.row_text(0), "DE");
assert_invariants(&vt);
}
#[test]
fn dch_at_wide_char_boundary() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed("A中B".as_bytes()); vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[1P"); assert_eq!(vt.row_text(0), "A B");
assert_invariants(&vt);
}
#[test]
fn ech_basic_erase() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[1;2H"); vt.feed(b"\x1b[3X"); assert_eq!(vt.row_text(0), "A E");
assert_eq!(vt.cursor(), (1, 0)); assert_invariants(&vt);
}
#[test]
fn ech_does_not_move_cursor() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[1X");
assert_eq!(vt.cursor(), (2, 0));
assert_eq!(vt.char_at(2, 0), Some(' '));
assert_eq!(vt.char_at(3, 0), Some('D'));
assert_invariants(&vt);
}
#[test]
fn ech_at_wide_char_continuation() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed("X中Y".as_bytes()); vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[1X"); assert_eq!(vt.row_text(0), "X Y");
assert_invariants(&vt);
}
#[test]
fn ech_clamped_to_line_end() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"ABCDE");
vt.feed(b"\x1b[1;4H"); vt.feed(b"\x1b[99X"); assert_eq!(vt.row_text(0), "ABC");
assert_invariants(&vt);
}
#[test]
fn il_basic_insert_line() {
let mut vt = VirtualTerminal::new(5, 5);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1L"); assert_eq!(vt.row_text(0), "AAAAA");
assert_eq!(vt.row_text(1), ""); assert_eq!(vt.row_text(2), "BBBBB");
assert_eq!(vt.row_text(3), "CCCCC");
assert_eq!(vt.row_text(4), "DDDDD");
}
#[test]
fn il_within_scroll_region() {
let mut vt = VirtualTerminal::new(5, 5);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1L"); assert_eq!(vt.row_text(0), "AAAAA"); assert_eq!(vt.row_text(1), ""); assert_eq!(vt.row_text(2), "BBBBB"); assert_eq!(vt.row_text(3), "CCCCC"); assert_eq!(vt.row_text(4), "EEEEE"); }
#[test]
fn il_outside_scroll_region_ignored() {
let mut vt = VirtualTerminal::new(5, 5);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[1;1H"); vt.feed(b"\x1b[1L"); assert_eq!(vt.row_text(0), "AAAAA");
assert_eq!(vt.row_text(1), "BBBBB");
assert_invariants(&vt);
}
#[test]
fn dl_basic_delete_line() {
let mut vt = VirtualTerminal::new(5, 5);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1M"); assert_eq!(vt.row_text(0), "AAAAA");
assert_eq!(vt.row_text(1), "CCCCC"); assert_eq!(vt.row_text(2), "DDDDD");
assert_eq!(vt.row_text(3), "EEEEE");
assert_eq!(vt.row_text(4), ""); }
#[test]
fn dl_within_scroll_region() {
let mut vt = VirtualTerminal::new(5, 5);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1M"); assert_eq!(vt.row_text(0), "AAAAA"); assert_eq!(vt.row_text(1), "CCCCC"); assert_eq!(vt.row_text(2), "DDDDD"); assert_eq!(vt.row_text(3), ""); assert_eq!(vt.row_text(4), "EEEEE"); }
#[test]
fn dl_outside_scroll_region_ignored() {
let mut vt = VirtualTerminal::new(5, 5);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[5;1H"); vt.feed(b"\x1b[1M"); assert_eq!(vt.row_text(4), "EEEEE");
}
#[test]
fn su_scroll_up_within_region() {
let mut vt = VirtualTerminal::new(5, 5);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[1S"); assert_eq!(vt.row_text(0), "AAAAA"); assert_eq!(vt.row_text(1), "CCCCC"); assert_eq!(vt.row_text(2), "DDDDD"); assert_eq!(vt.row_text(3), ""); assert_eq!(vt.row_text(4), "EEEEE"); assert_eq!(vt.scrollback_len(), 1);
assert_eq!(vt.scrollback_line(0), Some("BBBBB".to_string()));
}
#[test]
fn sd_scroll_down_within_region() {
let mut vt = VirtualTerminal::new(5, 5);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC\r\nDDDDD\r\nEEEEE");
vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[1T"); assert_eq!(vt.row_text(0), "AAAAA"); assert_eq!(vt.row_text(1), ""); assert_eq!(vt.row_text(2), "BBBBB"); assert_eq!(vt.row_text(3), "CCCCC"); assert_eq!(vt.row_text(4), "EEEEE"); }
#[test]
fn su_multiple_lines() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC");
vt.feed(b"\x1b[2S"); assert_eq!(vt.row_text(0), "CCCCC");
assert_eq!(vt.row_text(1), "");
assert_eq!(vt.row_text(2), "");
assert_eq!(vt.scrollback_len(), 2);
assert_eq!(vt.scrollback_line(0), Some("AAAAA".to_string()));
assert_eq!(vt.scrollback_line(1), Some("BBBBB".to_string()));
}
#[test]
fn rep_basic_repeat() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"X\x1b[3b"); assert_eq!(vt.row_text(0), "XXXX");
assert_eq!(vt.cursor(), (4, 0));
}
#[test]
fn rep_no_previous_char() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b[5b"); assert_eq!(vt.row_text(0), "");
assert_eq!(vt.cursor(), (0, 0));
}
#[test]
fn rep_wraps_across_lines() {
let mut vt = VirtualTerminal::new(5, 3);
vt.feed(b"A\x1b[6b"); assert_eq!(vt.row_text(0), "AAAAA");
assert_eq!(vt.row_text(1), "AA");
assert_eq!(vt.cursor(), (2, 1));
}
#[test]
fn decom_cup_relative_to_scroll_region() {
let mut vt = VirtualTerminal::new(10, 10);
vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[1;1H");
assert_eq!(vt.cursor(), (0, 2));
vt.feed(b"\x1b[3;5H");
assert_eq!(vt.cursor(), (4, 4));
}
#[test]
fn decom_clamps_to_scroll_region() {
let mut vt = VirtualTerminal::new(10, 10);
vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[99;1H");
assert_eq!(vt.cursor(), (0, 6)); }
#[test]
fn decom_disable_homes_to_origin() {
let mut vt = VirtualTerminal::new(10, 10);
vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[5;5H"); vt.feed(b"\x1b[?6l"); assert_eq!(vt.cursor(), (0, 0));
}
#[test]
fn decom_vpa_relative_to_scroll_region() {
let mut vt = VirtualTerminal::new(10, 10);
vt.feed(b"\x1b[3;7r"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[2d"); assert_eq!(vt.cursor().1, 3);
}
#[test]
fn decom_decstbm_homes_cursor() {
let mut vt = VirtualTerminal::new(10, 10);
vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[5;5H"); vt.feed(b"\x1b[3;7r"); assert_eq!(vt.cursor(), (0, 2)); }
#[test]
fn ss2_translates_one_char_from_g2() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b*0");
vt.feed(b"\x1bNq"); vt.feed(b"q"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{2500}'); assert_eq!(vt.char_at(1, 0).unwrap(), 'q');
}
#[test]
fn ss3_translates_one_char_from_g3() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b+0");
vt.feed(b"\x1bOx"); vt.feed(b"x"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{2502}'); assert_eq!(vt.char_at(1, 0).unwrap(), 'x');
}
#[test]
fn ss2_only_affects_one_character() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b*0"); vt.feed(b"\x1bNlk"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{250C}'); assert_eq!(vt.char_at(1, 0).unwrap(), 'k'); }
#[test]
fn ls2_invokes_g2_into_gl() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b*0"); vt.feed(b"\x1bn"); vt.feed(b"jm"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{2518}'); assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2514}'); }
#[test]
fn ls3_invokes_g3_into_gl() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b+0"); vt.feed(b"\x1bo"); vt.feed(b"n"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{253C}'); }
#[test]
fn ls2_persists_across_characters() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b*0"); vt.feed(b"\x1bn"); vt.feed(b"tuvw"); assert_eq!(vt.char_at(0, 0).unwrap(), '\u{251C}'); assert_eq!(vt.char_at(1, 0).unwrap(), '\u{2524}'); assert_eq!(vt.char_at(2, 0).unwrap(), '\u{2534}'); assert_eq!(vt.char_at(3, 0).unwrap(), '\u{252C}'); }
#[test]
fn alt_screen_1047_no_cursor_save() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"Main");
vt.feed(b"\x1b[1;5H"); let (_cx, _cy) = vt.cursor();
vt.feed(b"\x1b[?1047h"); assert!(vt.is_alternate_screen());
assert_eq!(vt.row_text(0), "");
vt.feed(b"Alt");
vt.feed(b"\x1b[?1047l"); assert!(!vt.is_alternate_screen());
assert_eq!(vt.row_text(0), "Main"); let (_, _) = vt.cursor();
}
#[test]
fn alt_screen_1047_double_enter_ignored() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"Main");
vt.feed(b"\x1b[?1047h"); vt.feed(b"\x1b[?1047h"); assert!(vt.is_alternate_screen());
assert_eq!(vt.row_text(0), ""); }
#[test]
fn decstbm_invalid_range_ignored() {
let mut vt = VirtualTerminal::new(10, 5);
vt.feed(b"\x1b[4;2r");
assert_eq!(vt.scroll_top, 0);
assert_eq!(vt.scroll_bottom, 4);
}
#[test]
fn decstbm_bottom_at_screen_edge() {
let mut vt = VirtualTerminal::new(10, 5);
vt.feed(b"\x1b[2;5r"); assert_eq!(vt.scroll_top, 1);
assert_eq!(vt.scroll_bottom, 4);
}
#[test]
fn decstbm_homes_cursor_without_decom() {
let mut vt = VirtualTerminal::new(10, 5);
vt.feed(b"\x1b[3;3H"); vt.feed(b"\x1b[2;4r"); assert_eq!(vt.cursor(), (0, 0));
}
#[test]
fn erase_display_mode1_from_start_to_cursor() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"AAAAAAAAAA");
vt.feed(b"BBBBBBBBBB");
vt.feed(b"CCCCCCCCCC");
vt.feed(b"\x1b[2;5H"); vt.feed(b"\x1b[1J"); assert_eq!(vt.row_text(0), ""); assert_eq!(vt.row_text(1), " BBBBB"); assert_eq!(vt.row_text(2), "CCCCCCCCCC"); }
#[test]
fn erase_display_mode3_clears_scrollback() {
let mut vt = VirtualTerminal::new(5, 2);
vt.feed(b"AAAAA\r\nBBBBB\r\nCCCCC"); assert!(vt.scrollback_len() > 0);
vt.feed(b"\x1b[3J"); assert_eq!(vt.scrollback_len(), 0);
}
#[test]
fn erase_line_mode1() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"ABCDEFGHIJ");
vt.feed(b"\x1b[1;6H"); vt.feed(b"\x1b[1K"); assert_eq!(vt.row_text(0), " GHIJ");
}
#[test]
fn soft_reset_restores_defaults() {
let mut vt = VirtualTerminal::new(10, 5);
vt.feed(b"\x1b[?7l"); vt.feed(b"\x1b[?25l"); vt.feed(b"\x1b[4h"); vt.feed(b"\x1b[?6h"); vt.feed(b"\x1b[2;4r"); vt.feed(b"\x1b[!p");
assert!(vt.cursor_visible());
assert_eq!(vt.scroll_top, 0);
assert_eq!(vt.scroll_bottom, 4);
vt.feed(b"\x1b[1;1H");
vt.feed(b"ABCDEFGHIJK");
assert_eq!(vt.row_text(0), "ABCDEFGHIJ");
assert_eq!(vt.row_text(1), "K"); }
#[test]
fn erase_line_splits_wide_char_at_boundary() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed("AB中DE".as_bytes()); vt.feed(b"\x1b[1;4H"); vt.feed(b"\x1b[K"); assert_invariants(&vt);
assert_eq!(vt.char_at(2, 0), Some(' '));
}
#[test]
fn dch_wide_char_continuation_at_boundary() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed("AB中DE".as_bytes());
vt.feed(b"\x1b[1;3H"); vt.feed(b"\x1b[2P"); assert_eq!(vt.row_text(0), "ABDE");
assert_invariants(&vt);
}
#[test]
fn invariants_after_insert_delete_scroll_sequence() {
let mut vt = VirtualTerminal::new(8, 4);
vt.feed(b"AABBCCDD");
vt.feed(b"EEFFGGHH");
vt.feed(b"IIJJKKLL");
vt.feed(b"MMNNOOPP");
vt.feed(b"\x1b[2;3r"); vt.feed(b"\x1b[2;1H"); vt.feed(b"\x1b[1L"); vt.feed(b"\x1b[1M"); vt.feed(b"\x1b[1S"); vt.feed(b"\x1b[1T"); vt.feed(b"\x1b[2;4H\x1b[2@"); vt.feed(b"\x1b[1P"); assert_invariants(&vt);
}
#[test]
fn invariants_after_wide_char_operations() {
let mut vt = VirtualTerminal::new(6, 3);
vt.feed("中文字".as_bytes()); vt.feed(b"\x1b[1;1H\x1b[2@"); assert_invariants(&vt);
let mut vt2 = VirtualTerminal::new(6, 3);
vt2.feed("中文字".as_bytes());
vt2.feed(b"\x1b[1;1H\x1b[2P"); assert_invariants(&vt2);
let mut vt3 = VirtualTerminal::new(6, 3);
vt3.feed("中文字".as_bytes());
vt3.feed(b"\x1b[1;3H\x1b[2X"); assert_invariants(&vt3);
}
#[test]
fn put_char_basic_ascii() {
let mut vt = VirtualTerminal::new(80, 24);
vt.put_char('H');
vt.put_char('e');
vt.put_char('l');
vt.put_char('l');
vt.put_char('o');
assert_eq!(vt.row_text(0), "Hello");
assert_eq!(vt.cursor(), (5, 0));
assert_invariants(&vt);
}
#[test]
fn put_char_matches_feed_str() {
let mut vt_feed = VirtualTerminal::new(40, 10);
vt_feed.feed_str("Hello!");
let mut vt_put = VirtualTerminal::new(40, 10);
for ch in "Hello!".chars() {
vt_put.put_char(ch);
}
assert_eq!(vt_feed.screen_text(), vt_put.screen_text());
assert_eq!(vt_feed.cursor(), vt_put.cursor());
}
#[test]
fn put_char_wide_characters() {
let mut vt = VirtualTerminal::new(10, 3);
vt.put_char('中');
vt.put_char('文');
assert_eq!(vt.row_text(0), "中文");
assert_eq!(vt.cursor(), (4, 0)); assert_invariants(&vt);
}
#[test]
fn put_char_autowrap() {
let mut vt = VirtualTerminal::new(5, 3);
for ch in "ABCDE".chars() {
vt.put_char(ch);
}
assert_eq!(vt.row_text(0), "ABCDE");
vt.put_char('F');
assert_eq!(vt.row_text(1), "F");
assert_eq!(vt.cursor(), (1, 1));
assert_invariants(&vt);
}
#[test]
fn put_char_wide_wrap_at_margin() {
let mut vt = VirtualTerminal::new(5, 3);
for ch in "ABCD".chars() {
vt.put_char(ch);
}
vt.put_char('中'); assert_eq!(vt.row_text(0), "ABCD");
assert_eq!(vt.row_text(1), "中");
assert_invariants(&vt);
}
#[test]
fn put_char_zero_width_skipped() {
let mut vt = VirtualTerminal::new(10, 3);
vt.put_char('A');
vt.put_char('\u{0300}'); assert_eq!(vt.cursor(), (1, 0)); assert_eq!(vt.char_at(0, 0), Some('A'));
assert_invariants(&vt);
}
#[test]
fn put_char_preserves_style() {
let mut vt = VirtualTerminal::new(10, 3);
vt.feed(b"\x1b[1m");
vt.put_char('X');
let style = vt.style_at(0, 0).unwrap();
assert!(style.bold);
assert_invariants(&vt);
}
#[test]
fn put_str_basic() {
let mut vt = VirtualTerminal::new(20, 3);
vt.put_str("Hello, world!");
assert_eq!(vt.row_text(0), "Hello, world!");
assert_eq!(vt.cursor(), (13, 0));
assert_invariants(&vt);
}
#[test]
fn put_str_empty() {
let mut vt = VirtualTerminal::new(10, 1);
vt.put_str("");
assert_eq!(vt.cursor(), (0, 0));
assert_eq!(vt.row_text(0), "");
assert_invariants(&vt);
}
#[test]
fn put_str_wraps_at_margin() {
let mut vt = VirtualTerminal::new(5, 3);
vt.put_str("ABCDEFGH");
assert_eq!(vt.row_text(0), "ABCDE");
assert_eq!(vt.row_text(1), "FGH");
assert_eq!(vt.cursor(), (3, 1));
assert_invariants(&vt);
}
#[test]
fn put_str_wide_chars() {
let mut vt = VirtualTerminal::new(10, 1);
vt.put_str("中文");
assert_eq!(vt.row_text(0), "中文");
assert_eq!(vt.cursor(), (4, 0));
assert_invariants(&vt);
}
#[test]
fn put_str_matches_put_char_sequence() {
let text = "Hi 😀!";
let mut vt_str = VirtualTerminal::new(20, 3);
vt_str.put_str(text);
let mut vt_char = VirtualTerminal::new(20, 3);
for ch in text.chars() {
vt_char.put_char(ch);
}
assert_eq!(vt_str.screen_text(), vt_char.screen_text());
assert_eq!(vt_str.cursor(), vt_char.cursor());
}
#[test]
fn put_str_does_not_interpret_escapes() {
let mut vt = VirtualTerminal::new(20, 1);
vt.put_str("\x1b[1mBold");
let text = vt.row_text(0);
assert!(text.contains("[1mBold"), "got: {text:?}");
let style = vt.style_at(0, 0).unwrap();
assert!(!style.bold);
assert_invariants(&vt);
}
#[test]
fn set_cursor_position_basic() {
let mut vt = VirtualTerminal::new(80, 24);
vt.set_cursor_position(10, 5);
assert_eq!(vt.cursor(), (10, 5));
assert_invariants(&vt);
}
#[test]
fn set_cursor_position_clamps_x() {
let mut vt = VirtualTerminal::new(80, 24);
vt.set_cursor_position(200, 5);
assert_eq!(vt.cursor(), (79, 5));
assert_invariants(&vt);
}
#[test]
fn set_cursor_position_clamps_y() {
let mut vt = VirtualTerminal::new(80, 24);
vt.set_cursor_position(10, 100);
assert_eq!(vt.cursor(), (10, 23));
assert_invariants(&vt);
}
#[test]
fn set_cursor_position_origin() {
let mut vt = VirtualTerminal::new(80, 24);
vt.put_str("test");
vt.set_cursor_position(0, 0);
assert_eq!(vt.cursor(), (0, 0));
assert_invariants(&vt);
}
#[test]
fn set_cursor_position_then_put_char() {
let mut vt = VirtualTerminal::new(10, 3);
vt.set_cursor_position(3, 1);
vt.put_char('X');
assert_eq!(vt.char_at(3, 1), Some('X'));
assert_eq!(vt.cursor(), (4, 1));
assert_invariants(&vt);
}
#[test]
fn clear_empties_display() {
let mut vt = VirtualTerminal::new(10, 3);
vt.put_str("Hello");
vt.set_cursor_position(0, 1);
vt.put_str("World");
vt.clear();
assert_eq!(vt.row_text(0), "");
assert_eq!(vt.row_text(1), "");
assert_invariants(&vt);
}
#[test]
fn clear_preserves_cursor_position() {
let mut vt = VirtualTerminal::new(10, 3);
vt.set_cursor_position(5, 2);
vt.clear();
assert_eq!(vt.cursor(), (5, 2));
assert_invariants(&vt);
}
#[test]
fn clear_does_not_affect_scrollback() {
let mut vt = VirtualTerminal::new(5, 2);
vt.put_str("AAAAABBBBBCCCCC");
let sb_before = vt.scrollback_len();
assert!(sb_before > 0);
vt.clear();
assert_eq!(vt.scrollback_len(), sb_before);
assert_invariants(&vt);
}
#[test]
fn clear_scrollback_empties_history() {
let mut vt = VirtualTerminal::new(5, 2);
vt.put_str("AAAAABBBBBCCCCC");
assert!(vt.scrollback_len() > 0);
vt.clear_scrollback();
assert_eq!(vt.scrollback_len(), 0);
assert_invariants(&vt);
}
#[test]
fn clear_scrollback_preserves_display() {
let mut vt = VirtualTerminal::new(10, 2);
vt.put_str("Hello");
vt.set_cursor_position(0, 1);
vt.put_str("World");
vt.clear_scrollback();
assert_eq!(vt.row_text(0), "Hello");
assert_eq!(vt.row_text(1), "World");
assert_invariants(&vt);
}
}