From a15ad9d758fcb8411e390cfc50f7fc9e6a62568f Mon Sep 17 00:00:00 2001 From: JEEVITHA KANNAN K S Date: Wed, 2 Oct 2024 21:17:10 +0530 Subject: [PATCH] Added vt100 patch --- Cargo.lock | 2 - Cargo.toml | 3 + tui/src/running_command.rs | 2 +- vt100/Cargo.toml | 29 + vt100/LICENSE | 21 + vt100/src/attrs.rs | 128 +++ vt100/src/callbacks.rs | 16 + vt100/src/cell.rs | 166 ++++ vt100/src/grid.rs | 735 ++++++++++++++++++ vt100/src/lib.rs | 61 ++ vt100/src/parser.rs | 78 ++ vt100/src/perform.rs | 305 ++++++++ vt100/src/row.rs | 474 +++++++++++ vt100/src/screen.rs | 1511 ++++++++++++++++++++++++++++++++++++ vt100/src/term.rs | 592 ++++++++++++++ 15 files changed, 4120 insertions(+), 3 deletions(-) create mode 100644 vt100/Cargo.toml create mode 100644 vt100/LICENSE create mode 100644 vt100/src/attrs.rs create mode 100644 vt100/src/callbacks.rs create mode 100644 vt100/src/cell.rs create mode 100644 vt100/src/grid.rs create mode 100644 vt100/src/lib.rs create mode 100644 vt100/src/parser.rs create mode 100644 vt100/src/perform.rs create mode 100644 vt100/src/row.rs create mode 100644 vt100/src/screen.rs create mode 100644 vt100/src/term.rs diff --git a/Cargo.lock b/Cargo.lock index c33834f66..4a4894e3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1235,8 +1235,6 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vt100" version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de" dependencies = [ "itoa", "log", diff --git a/Cargo.toml b/Cargo.toml index 27257d9ca..a31cc015f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,9 @@ ego-tree = "0.6.2" members = ["tui", "core"] resolver = "2" +[patch.crates-io] +vt100 = { path = "vt100" } + [profile.release] opt-level = "z" debug = false diff --git a/tui/src/running_command.rs b/tui/src/running_command.rs index ce7bee02c..89daa7555 100644 --- a/tui/src/running_command.rs +++ b/tui/src/running_command.rs @@ -258,7 +258,7 @@ impl RunningCommand { let buffer = mutex.as_ref().unwrap(); parser.process(buffer); // Adjust the screen content based on the scroll offset - parser.set_scrollback(self.scroll_offset); + parser.screen_mut().set_scrollback(self.scroll_offset); parser.screen().clone() } diff --git a/vt100/Cargo.toml b/vt100/Cargo.toml new file mode 100644 index 000000000..ce8f9af18 --- /dev/null +++ b/vt100/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "vt100" +version = "0.15.2" +authors = ["Jesse Luehrs "] +edition = "2021" + +description = "Library for parsing terminal data" +homepage = "https://github.com/doy/vt100-rust" +repository = "https://github.com/doy/vt100-rust" +readme = "README.md" +keywords = ["terminal", "vt100"] +categories = ["command-line-interface", "encoding"] +license = "MIT" +include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"] + +[dependencies] +itoa = "1.0.9" +log = "0.4.19" +unicode-width = "0.1.10" +vte = "0.11.1" + +[dev-dependencies] +nix = "0.26.2" +quickcheck = "1.0" +rand = "0.8" +serde = { version = "1.0.182", features = ["derive"] } +serde_json = "1.0.104" +terminal_size = "0.2.6" +vte = "0.11.1" diff --git a/vt100/LICENSE b/vt100/LICENSE new file mode 100644 index 000000000..a1dd5b0e5 --- /dev/null +++ b/vt100/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Jesse Luehrs + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vt100/src/attrs.rs b/vt100/src/attrs.rs new file mode 100644 index 000000000..e5ee3fbac --- /dev/null +++ b/vt100/src/attrs.rs @@ -0,0 +1,128 @@ +use crate::term::BufWrite as _; + +/// Represents a foreground or background color for cells. +#[derive(Eq, PartialEq, Debug, Copy, Clone)] +pub enum Color { + /// The default terminal color. + Default, + + /// An indexed terminal color. + Idx(u8), + + /// An RGB terminal color. The parameters are (red, green, blue). + Rgb(u8, u8, u8), +} + +impl Default for Color { + fn default() -> Self { + Self::Default + } +} + +const TEXT_MODE_BOLD: u8 = 0b0000_0001; +const TEXT_MODE_ITALIC: u8 = 0b0000_0010; +const TEXT_MODE_UNDERLINE: u8 = 0b0000_0100; +const TEXT_MODE_INVERSE: u8 = 0b0000_1000; + +#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] +pub struct Attrs { + pub fgcolor: Color, + pub bgcolor: Color, + pub mode: u8, +} + +impl Attrs { + pub fn bold(&self) -> bool { + self.mode & TEXT_MODE_BOLD != 0 + } + + pub fn set_bold(&mut self, bold: bool) { + if bold { + self.mode |= TEXT_MODE_BOLD; + } else { + self.mode &= !TEXT_MODE_BOLD; + } + } + + pub fn italic(&self) -> bool { + self.mode & TEXT_MODE_ITALIC != 0 + } + + pub fn set_italic(&mut self, italic: bool) { + if italic { + self.mode |= TEXT_MODE_ITALIC; + } else { + self.mode &= !TEXT_MODE_ITALIC; + } + } + + pub fn underline(&self) -> bool { + self.mode & TEXT_MODE_UNDERLINE != 0 + } + + pub fn set_underline(&mut self, underline: bool) { + if underline { + self.mode |= TEXT_MODE_UNDERLINE; + } else { + self.mode &= !TEXT_MODE_UNDERLINE; + } + } + + pub fn inverse(&self) -> bool { + self.mode & TEXT_MODE_INVERSE != 0 + } + + pub fn set_inverse(&mut self, inverse: bool) { + if inverse { + self.mode |= TEXT_MODE_INVERSE; + } else { + self.mode &= !TEXT_MODE_INVERSE; + } + } + + pub fn write_escape_code_diff( + &self, + contents: &mut Vec, + other: &Self, + ) { + if self != other && self == &Self::default() { + crate::term::ClearAttrs.write_buf(contents); + return; + } + + let attrs = crate::term::Attrs::default(); + + let attrs = if self.fgcolor == other.fgcolor { + attrs + } else { + attrs.fgcolor(self.fgcolor) + }; + let attrs = if self.bgcolor == other.bgcolor { + attrs + } else { + attrs.bgcolor(self.bgcolor) + }; + let attrs = if self.bold() == other.bold() { + attrs + } else { + attrs.bold(self.bold()) + }; + let attrs = if self.italic() == other.italic() { + attrs + } else { + attrs.italic(self.italic()) + }; + let attrs = if self.underline() == other.underline() { + attrs + } else { + attrs.underline(self.underline()) + }; + let attrs = if self.inverse() == other.inverse() { + attrs + } else { + attrs.inverse(self.inverse()) + }; + + attrs.write_buf(contents); + } +} diff --git a/vt100/src/callbacks.rs b/vt100/src/callbacks.rs new file mode 100644 index 000000000..626290633 --- /dev/null +++ b/vt100/src/callbacks.rs @@ -0,0 +1,16 @@ +/// This trait is used with `Parser::process_cb` to handle extra escape +/// sequences that don't have an impact on the terminal screen directly. +pub trait Callbacks { + /// This callback is called when the terminal requests an audible bell + /// (typically with `^G`). + fn audible_bell(&mut self, _: &mut crate::Screen) {} + /// This callback is called when the terminal requests an visual bell + /// (typically with `\eg`). + fn visual_bell(&mut self, _: &mut crate::Screen) {} + /// This callback is called when the terminal requests a resize + /// (typically with `\e[8;;t`). + fn resize(&mut self, _: &mut crate::Screen, _request: (u16, u16)) {} + /// This callback is called when the terminal receives invalid input + /// (such as an invalid UTF-8 character or an unused control character). + fn error(&mut self, _: &mut crate::Screen) {} +} diff --git a/vt100/src/cell.rs b/vt100/src/cell.rs new file mode 100644 index 000000000..b7ddd2666 --- /dev/null +++ b/vt100/src/cell.rs @@ -0,0 +1,166 @@ +use unicode_width::UnicodeWidthChar as _; + +const CODEPOINTS_IN_CELL: usize = 6; + +/// Represents a single terminal cell. +#[derive(Clone, Debug, Eq)] +pub struct Cell { + contents: [char; CODEPOINTS_IN_CELL], + len: u8, + attrs: crate::attrs::Attrs, +} + +impl PartialEq for Cell { + fn eq(&self, other: &Self) -> bool { + if self.len != other.len { + return false; + } + if self.attrs != other.attrs { + return false; + } + let len = self.len(); + // self.len() always returns a valid value + self.contents[..len] == other.contents[..len] + } +} + +impl Cell { + pub(crate) fn new() -> Self { + Self { + contents: Default::default(), + len: 0, + attrs: crate::attrs::Attrs::default(), + } + } + + #[inline] + fn len(&self) -> usize { + usize::from(self.len & 0x0f) + } + + pub(crate) fn set(&mut self, c: char, a: crate::attrs::Attrs) { + self.contents[0] = c; + self.len = 1; + // strings in this context should always be an arbitrary character + // followed by zero or more zero-width characters, so we should only + // have to look at the first character + self.set_wide(c.width().unwrap_or(1) > 1); + self.attrs = a; + } + + pub(crate) fn append(&mut self, c: char) { + let len = self.len(); + if len >= CODEPOINTS_IN_CELL { + return; + } + if len == 0 { + // 0 is always less than 6 + self.contents[0] = ' '; + self.len += 1; + } + + let len = self.len(); + // we already checked that len < CODEPOINTS_IN_CELL + self.contents[len] = c; + self.len += 1; + } + + pub(crate) fn clear(&mut self, attrs: crate::attrs::Attrs) { + self.len = 0; + self.attrs = attrs; + } + + /// Returns the text contents of the cell. + /// + /// Can include multiple unicode characters if combining characters are + /// used, but will contain at most one character with a non-zero character + /// width. + #[must_use] + pub fn contents(&self) -> String { + let mut s = String::with_capacity(CODEPOINTS_IN_CELL * 4); + for c in self.contents.iter().take(self.len()) { + s.push(*c); + } + s + } + + /// Returns whether the cell contains any text data. + #[must_use] + pub fn has_contents(&self) -> bool { + self.len > 0 + } + + /// Returns whether the text data in the cell represents a wide character. + #[must_use] + pub fn is_wide(&self) -> bool { + self.len & 0x80 == 0x80 + } + + /// Returns whether the cell contains the second half of a wide character + /// (in other words, whether the previous cell in the row contains a wide + /// character) + #[must_use] + pub fn is_wide_continuation(&self) -> bool { + self.len & 0x40 == 0x40 + } + + fn set_wide(&mut self, wide: bool) { + if wide { + self.len |= 0x80; + } else { + self.len &= 0x7f; + } + } + + pub(crate) fn set_wide_continuation(&mut self, wide: bool) { + if wide { + self.len |= 0x40; + } else { + self.len &= 0xbf; + } + } + + pub(crate) fn attrs(&self) -> &crate::attrs::Attrs { + &self.attrs + } + + /// Returns the foreground color of the cell. + #[must_use] + pub fn fgcolor(&self) -> crate::Color { + self.attrs.fgcolor + } + + /// Returns the background color of the cell. + #[must_use] + pub fn bgcolor(&self) -> crate::Color { + self.attrs.bgcolor + } + + /// Returns whether the cell should be rendered with the bold text + /// attribute. + #[must_use] + pub fn bold(&self) -> bool { + self.attrs.bold() + } + + /// Returns whether the cell should be rendered with the italic text + /// attribute. + #[must_use] + pub fn italic(&self) -> bool { + self.attrs.italic() + } + + /// Returns whether the cell should be rendered with the underlined text + /// attribute. + #[must_use] + pub fn underline(&self) -> bool { + self.attrs.underline() + } + + /// Returns whether the cell should be rendered with the inverse text + /// attribute. + #[must_use] + pub fn inverse(&self) -> bool { + self.attrs.inverse() + } +} diff --git a/vt100/src/grid.rs b/vt100/src/grid.rs new file mode 100644 index 000000000..29d3aad53 --- /dev/null +++ b/vt100/src/grid.rs @@ -0,0 +1,735 @@ +use crate::term::BufWrite as _; + +#[derive(Clone, Debug)] +pub struct Grid { + size: Size, + pos: Pos, + saved_pos: Pos, + rows: Vec, + scroll_top: u16, + scroll_bottom: u16, + origin_mode: bool, + saved_origin_mode: bool, + scrollback: std::collections::VecDeque, + scrollback_len: usize, + scrollback_offset: usize, +} + +impl Grid { + pub fn new(size: Size, scrollback_len: usize) -> Self { + Self { + size, + pos: Pos::default(), + saved_pos: Pos::default(), + rows: vec![], + scroll_top: 0, + scroll_bottom: size.rows - 1, + origin_mode: false, + saved_origin_mode: false, + scrollback: std::collections::VecDeque::new(), + scrollback_len, + scrollback_offset: 0, + } + } + + pub fn allocate_rows(&mut self) { + if self.rows.is_empty() { + self.rows.extend( + std::iter::repeat_with(|| { + crate::row::Row::new(self.size.cols) + }) + .take(usize::from(self.size.rows)), + ); + } + } + + fn new_row(&self) -> crate::row::Row { + crate::row::Row::new(self.size.cols) + } + + pub fn clear(&mut self) { + self.pos = Pos::default(); + self.saved_pos = Pos::default(); + for row in self.drawing_rows_mut() { + row.clear(crate::attrs::Attrs::default()); + } + self.scroll_top = 0; + self.scroll_bottom = self.size.rows - 1; + self.origin_mode = false; + self.saved_origin_mode = false; + } + + pub fn size(&self) -> Size { + self.size + } + + pub fn set_size(&mut self, size: Size) { + if size.cols != self.size.cols { + for row in &mut self.rows { + row.wrap(false); + } + } + + if self.scroll_bottom == self.size.rows - 1 { + self.scroll_bottom = size.rows - 1; + } + + self.size = size; + for row in &mut self.rows { + row.resize(size.cols, crate::Cell::new()); + } + self.rows.resize(usize::from(size.rows), self.new_row()); + + if self.scroll_bottom >= size.rows { + self.scroll_bottom = size.rows - 1; + } + if self.scroll_bottom < self.scroll_top { + self.scroll_top = 0; + } + + self.row_clamp_top(false); + self.row_clamp_bottom(false); + self.col_clamp(); + } + + pub fn pos(&self) -> Pos { + self.pos + } + + pub fn set_pos(&mut self, mut pos: Pos) { + if self.origin_mode { + pos.row = pos.row.saturating_add(self.scroll_top); + } + self.pos = pos; + self.row_clamp_top(self.origin_mode); + self.row_clamp_bottom(self.origin_mode); + self.col_clamp(); + } + + pub fn save_cursor(&mut self) { + self.saved_pos = self.pos; + self.saved_origin_mode = self.origin_mode; + } + + pub fn restore_cursor(&mut self) { + self.pos = self.saved_pos; + self.origin_mode = self.saved_origin_mode; + } + + pub fn visible_rows(&self) -> impl Iterator { + let scrollback_len = self.scrollback.len(); + let rows_len = self.rows.len(); + self.scrollback + .iter() + .skip(scrollback_len - self.scrollback_offset) + // when scrollback_offset > rows_len (e.g. rows = 3, + // scrollback_len = 10, offset = 9) the skip(10 - 9) + // will take 9 rows instead of 3. we need to set + // the upper bound to rows_len (e.g. 3) + .take(rows_len) + // same for rows_len - scrollback_offset (e.g. 3 - 9). + // it'll panic with overflow. we have to saturate the subtraction. + .chain(self.rows.iter().take(rows_len.saturating_sub(self.scrollback_offset))) + } + + pub fn drawing_rows(&self) -> impl Iterator { + self.rows.iter() + } + + pub fn drawing_rows_mut( + &mut self, + ) -> impl Iterator { + self.rows.iter_mut() + } + + pub fn visible_row(&self, row: u16) -> Option<&crate::row::Row> { + self.visible_rows().nth(usize::from(row)) + } + + pub fn drawing_row(&self, row: u16) -> Option<&crate::row::Row> { + self.drawing_rows().nth(usize::from(row)) + } + + pub fn drawing_row_mut( + &mut self, + row: u16, + ) -> Option<&mut crate::row::Row> { + self.drawing_rows_mut().nth(usize::from(row)) + } + + pub fn current_row_mut(&mut self) -> &mut crate::row::Row { + self.drawing_row_mut(self.pos.row) + // we assume self.pos.row is always valid + .unwrap() + } + + pub fn visible_cell(&self, pos: Pos) -> Option<&crate::Cell> { + self.visible_row(pos.row).and_then(|r| r.get(pos.col)) + } + + pub fn drawing_cell(&self, pos: Pos) -> Option<&crate::Cell> { + self.drawing_row(pos.row).and_then(|r| r.get(pos.col)) + } + + pub fn drawing_cell_mut(&mut self, pos: Pos) -> Option<&mut crate::Cell> { + self.drawing_row_mut(pos.row) + .and_then(|r| r.get_mut(pos.col)) + } + + pub fn scrollback_len(&self) -> usize { + self.scrollback_len + } + + pub fn scrollback(&self) -> usize { + self.scrollback_offset + } + + pub fn set_scrollback(&mut self, rows: usize) { + self.scrollback_offset = rows.min(self.scrollback.len()); + } + + pub fn write_contents(&self, contents: &mut String) { + let mut wrapping = false; + for row in self.visible_rows() { + row.write_contents(contents, 0, self.size.cols, wrapping); + if !row.wrapped() { + contents.push('\n'); + } + wrapping = row.wrapped(); + } + + while contents.ends_with('\n') { + contents.truncate(contents.len() - 1); + } + } + + pub fn write_contents_formatted( + &self, + contents: &mut Vec, + ) -> crate::attrs::Attrs { + crate::term::ClearAttrs.write_buf(contents); + crate::term::ClearScreen.write_buf(contents); + + let mut prev_attrs = crate::attrs::Attrs::default(); + let mut prev_pos = Pos::default(); + let mut wrapping = false; + for (i, row) in self.visible_rows().enumerate() { + // we limit the number of cols to a u16 (see Size), so + // visible_rows() can never return more rows than will fit + let i = i.try_into().unwrap(); + let (new_pos, new_attrs) = row.write_contents_formatted( + contents, + 0, + self.size.cols, + i, + wrapping, + Some(prev_pos), + Some(prev_attrs), + ); + prev_pos = new_pos; + prev_attrs = new_attrs; + wrapping = row.wrapped(); + } + + self.write_cursor_position_formatted( + contents, + Some(prev_pos), + Some(prev_attrs), + ); + + prev_attrs + } + + pub fn write_contents_diff( + &self, + contents: &mut Vec, + prev: &Self, + mut prev_attrs: crate::attrs::Attrs, + ) -> crate::attrs::Attrs { + let mut prev_pos = prev.pos; + let mut wrapping = false; + let mut prev_wrapping = false; + for (i, (row, prev_row)) in + self.visible_rows().zip(prev.visible_rows()).enumerate() + { + // we limit the number of cols to a u16 (see Size), so + // visible_rows() can never return more rows than will fit + let i = i.try_into().unwrap(); + let (new_pos, new_attrs) = row.write_contents_diff( + contents, + prev_row, + 0, + self.size.cols, + i, + wrapping, + prev_wrapping, + prev_pos, + prev_attrs, + ); + prev_pos = new_pos; + prev_attrs = new_attrs; + wrapping = row.wrapped(); + prev_wrapping = prev_row.wrapped(); + } + + self.write_cursor_position_formatted( + contents, + Some(prev_pos), + Some(prev_attrs), + ); + + prev_attrs + } + + pub fn write_cursor_position_formatted( + &self, + contents: &mut Vec, + prev_pos: Option, + prev_attrs: Option, + ) { + let prev_attrs = prev_attrs.unwrap_or_default(); + // writing a character to the last column of a row doesn't wrap the + // cursor immediately - it waits until the next character is actually + // drawn. it is only possible for the cursor to have this kind of + // position after drawing a character though, so if we end in this + // position, we need to redraw the character at the end of the row. + if prev_pos != Some(self.pos) && self.pos.col >= self.size.cols { + let mut pos = Pos { + row: self.pos.row, + col: self.size.cols - 1, + }; + if self + .drawing_cell(pos) + // we assume self.pos.row is always valid, and self.size.cols + // - 1 is always a valid column + .unwrap() + .is_wide_continuation() + { + pos.col = self.size.cols - 2; + } + let cell = + // we assume self.pos.row is always valid, and self.size.cols + // - 2 must be a valid column because self.size.cols - 1 is + // always valid and we just checked that the cell at + // self.size.cols - 1 is a wide continuation character, which + // means that the first half of the wide character must be + // before it + self.drawing_cell(pos).unwrap(); + if cell.has_contents() { + if let Some(prev_pos) = prev_pos { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + } else { + crate::term::MoveTo::new(pos).write_buf(contents); + } + cell.attrs().write_escape_code_diff(contents, &prev_attrs); + contents.extend(cell.contents().as_bytes()); + prev_attrs.write_escape_code_diff(contents, cell.attrs()); + } else { + // if the cell doesn't have contents, we can't have gotten + // here by drawing a character in the last column. this means + // that as far as i'm aware, we have to have reached here from + // a newline when we were already after the end of an earlier + // row. in the case where we are already after the end of an + // earlier row, we can just write a few newlines, otherwise we + // also need to do the same as above to get ourselves to after + // the end of a row. + let mut found = false; + for i in (0..self.pos.row).rev() { + pos.row = i; + pos.col = self.size.cols - 1; + if self + .drawing_cell(pos) + // i is always less than self.pos.row, which we assume + // to be always valid, so it must also be valid. + // self.size.cols - 1 is always a valid col. + .unwrap() + .is_wide_continuation() + { + pos.col = self.size.cols - 2; + } + let cell = self + .drawing_cell(pos) + // i is always less than self.pos.row, which we assume + // to be always valid, so it must also be valid. + // self.size.cols - 2 is valid because self.size.cols + // - 1 is always valid, and col gets set to + // self.size.cols - 2 when the cell at self.size.cols + // - 1 is a wide continuation character, meaning that + // the first half of the wide character must be before + // it + .unwrap(); + if cell.has_contents() { + if let Some(prev_pos) = prev_pos { + if prev_pos.row != i + || prev_pos.col < self.size.cols + { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + cell.attrs().write_escape_code_diff( + contents, + &prev_attrs, + ); + contents.extend(cell.contents().as_bytes()); + prev_attrs.write_escape_code_diff( + contents, + cell.attrs(), + ); + } + } else { + crate::term::MoveTo::new(pos).write_buf(contents); + cell.attrs().write_escape_code_diff( + contents, + &prev_attrs, + ); + contents.extend(cell.contents().as_bytes()); + prev_attrs.write_escape_code_diff( + contents, + cell.attrs(), + ); + } + contents.extend( + "\n".repeat(usize::from(self.pos.row - i)) + .as_bytes(), + ); + found = true; + break; + } + } + + // this can happen if you get the cursor off the end of a row, + // and then do something to clear the end of the current row + // without moving the cursor (IL, DL, ED, EL, etc). we know + // there can't be something in the last column because we + // would have caught that above, so it should be safe to + // overwrite it. + if !found { + pos = Pos { + row: self.pos.row, + col: self.size.cols - 1, + }; + if let Some(prev_pos) = prev_pos { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + } else { + crate::term::MoveTo::new(pos).write_buf(contents); + } + contents.push(b' '); + // we know that the cell has no contents, but it still may + // have drawing attributes (background color, etc) + let end_cell = self + .drawing_cell(pos) + // we assume self.pos.row is always valid, and + // self.size.cols - 1 is always a valid column + .unwrap(); + end_cell + .attrs() + .write_escape_code_diff(contents, &prev_attrs); + crate::term::SaveCursor.write_buf(contents); + crate::term::Backspace.write_buf(contents); + crate::term::EraseChar::new(1).write_buf(contents); + crate::term::RestoreCursor.write_buf(contents); + prev_attrs + .write_escape_code_diff(contents, end_cell.attrs()); + } + } + } else if let Some(prev_pos) = prev_pos { + crate::term::MoveFromTo::new(prev_pos, self.pos) + .write_buf(contents); + } else { + crate::term::MoveTo::new(self.pos).write_buf(contents); + } + } + + pub fn erase_all(&mut self, attrs: crate::attrs::Attrs) { + for row in self.drawing_rows_mut() { + row.clear(attrs); + } + } + + pub fn erase_all_forward(&mut self, attrs: crate::attrs::Attrs) { + let pos = self.pos; + for row in self.drawing_rows_mut().skip(usize::from(pos.row) + 1) { + row.clear(attrs); + } + + self.erase_row_forward(attrs); + } + + pub fn erase_all_backward(&mut self, attrs: crate::attrs::Attrs) { + let pos = self.pos; + for row in self.drawing_rows_mut().take(usize::from(pos.row)) { + row.clear(attrs); + } + + self.erase_row_backward(attrs); + } + + pub fn erase_row(&mut self, attrs: crate::attrs::Attrs) { + self.current_row_mut().clear(attrs); + } + + pub fn erase_row_forward(&mut self, attrs: crate::attrs::Attrs) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for col in pos.col..size.cols { + row.erase(col, attrs); + } + } + + pub fn erase_row_backward(&mut self, attrs: crate::attrs::Attrs) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for col in 0..=pos.col.min(size.cols - 1) { + row.erase(col, attrs); + } + } + + pub fn insert_cells(&mut self, count: u16) { + let size = self.size; + let pos = self.pos; + let wide = pos.col < size.cols + && self + .drawing_cell(pos) + // we assume self.pos.row is always valid, and we know we are + // not off the end of a row because we just checked pos.col < + // size.cols + .unwrap() + .is_wide_continuation(); + let row = self.current_row_mut(); + for _ in 0..count { + if wide { + row.get_mut(pos.col).unwrap().set_wide_continuation(false); + } + row.insert(pos.col, crate::Cell::new()); + if wide { + row.get_mut(pos.col).unwrap().set_wide_continuation(true); + } + } + row.truncate(size.cols); + } + + pub fn delete_cells(&mut self, count: u16) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for _ in 0..(count.min(size.cols - pos.col)) { + row.remove(pos.col); + } + row.resize(size.cols, crate::Cell::new()); + } + + pub fn erase_cells(&mut self, count: u16, attrs: crate::attrs::Attrs) { + let size = self.size; + let pos = self.pos; + let row = self.current_row_mut(); + for col in pos.col..((pos.col.saturating_add(count)).min(size.cols)) { + row.erase(col, attrs); + } + } + + pub fn insert_lines(&mut self, count: u16) { + for _ in 0..count { + self.rows.remove(usize::from(self.scroll_bottom)); + self.rows.insert(usize::from(self.pos.row), self.new_row()); + // self.scroll_bottom is maintained to always be a valid row + self.rows[usize::from(self.scroll_bottom)].wrap(false); + } + } + + pub fn delete_lines(&mut self, count: u16) { + for _ in 0..(count.min(self.size.rows - self.pos.row)) { + self.rows + .insert(usize::from(self.scroll_bottom) + 1, self.new_row()); + self.rows.remove(usize::from(self.pos.row)); + } + } + + pub fn scroll_up(&mut self, count: u16) { + for _ in 0..(count.min(self.size.rows - self.scroll_top)) { + self.rows + .insert(usize::from(self.scroll_bottom) + 1, self.new_row()); + let removed = self.rows.remove(usize::from(self.scroll_top)); + if self.scrollback_len > 0 && !self.scroll_region_active() { + self.scrollback.push_back(removed); + while self.scrollback.len() > self.scrollback_len { + self.scrollback.pop_front(); + } + if self.scrollback_offset > 0 { + self.scrollback_offset = + self.scrollback.len().min(self.scrollback_offset + 1); + } + } + } + } + + pub fn scroll_down(&mut self, count: u16) { + for _ in 0..count { + self.rows.remove(usize::from(self.scroll_bottom)); + self.rows + .insert(usize::from(self.scroll_top), self.new_row()); + // self.scroll_bottom is maintained to always be a valid row + self.rows[usize::from(self.scroll_bottom)].wrap(false); + } + } + + pub fn set_scroll_region(&mut self, top: u16, bottom: u16) { + let bottom = bottom.min(self.size().rows - 1); + if top < bottom { + self.scroll_top = top; + self.scroll_bottom = bottom; + } else { + self.scroll_top = 0; + self.scroll_bottom = self.size().rows - 1; + } + self.pos.row = self.scroll_top; + self.pos.col = 0; + } + + fn in_scroll_region(&self) -> bool { + self.pos.row >= self.scroll_top && self.pos.row <= self.scroll_bottom + } + + fn scroll_region_active(&self) -> bool { + self.scroll_top != 0 || self.scroll_bottom != self.size.rows - 1 + } + + pub fn set_origin_mode(&mut self, mode: bool) { + self.origin_mode = mode; + self.set_pos(Pos { row: 0, col: 0 }); + } + + pub fn row_inc_clamp(&mut self, count: u16) { + let in_scroll_region = self.in_scroll_region(); + self.pos.row = self.pos.row.saturating_add(count); + self.row_clamp_bottom(in_scroll_region); + } + + pub fn row_inc_scroll(&mut self, count: u16) -> u16 { + let in_scroll_region = self.in_scroll_region(); + self.pos.row = self.pos.row.saturating_add(count); + let lines = self.row_clamp_bottom(in_scroll_region); + if in_scroll_region { + self.scroll_up(lines); + lines + } else { + 0 + } + } + + pub fn row_dec_clamp(&mut self, count: u16) { + let in_scroll_region = self.in_scroll_region(); + self.pos.row = self.pos.row.saturating_sub(count); + self.row_clamp_top(in_scroll_region); + } + + pub fn row_dec_scroll(&mut self, count: u16) { + let in_scroll_region = self.in_scroll_region(); + // need to account for clamping by both row_clamp_top and by + // saturating_sub + let extra_lines = if count > self.pos.row { + count - self.pos.row + } else { + 0 + }; + self.pos.row = self.pos.row.saturating_sub(count); + let lines = self.row_clamp_top(in_scroll_region); + self.scroll_down(lines + extra_lines); + } + + pub fn row_set(&mut self, i: u16) { + self.pos.row = i; + self.row_clamp(); + } + + pub fn col_inc(&mut self, count: u16) { + self.pos.col = self.pos.col.saturating_add(count); + } + + pub fn col_inc_clamp(&mut self, count: u16) { + self.pos.col = self.pos.col.saturating_add(count); + self.col_clamp(); + } + + pub fn col_dec(&mut self, count: u16) { + self.pos.col = self.pos.col.saturating_sub(count); + } + + pub fn col_tab(&mut self) { + self.pos.col -= self.pos.col % 8; + self.pos.col += 8; + self.col_clamp(); + } + + pub fn col_set(&mut self, i: u16) { + self.pos.col = i; + self.col_clamp(); + } + + pub fn col_wrap(&mut self, width: u16, wrap: bool) { + if self.pos.col > self.size.cols - width { + let mut prev_pos = self.pos; + self.pos.col = 0; + let scrolled = self.row_inc_scroll(1); + prev_pos.row -= scrolled; + let new_pos = self.pos; + self.drawing_row_mut(prev_pos.row) + // we assume self.pos.row is always valid, and so prev_pos.row + // must be valid because it is always less than or equal to + // self.pos.row + .unwrap() + .wrap(wrap && prev_pos.row + 1 == new_pos.row); + } + } + + fn row_clamp_top(&mut self, limit_to_scroll_region: bool) -> u16 { + if limit_to_scroll_region && self.pos.row < self.scroll_top { + let rows = self.scroll_top - self.pos.row; + self.pos.row = self.scroll_top; + rows + } else { + 0 + } + } + + fn row_clamp_bottom(&mut self, limit_to_scroll_region: bool) -> u16 { + let bottom = if limit_to_scroll_region { + self.scroll_bottom + } else { + self.size.rows - 1 + }; + if self.pos.row > bottom { + let rows = self.pos.row - bottom; + self.pos.row = bottom; + rows + } else { + 0 + } + } + + fn row_clamp(&mut self) { + if self.pos.row > self.size.rows - 1 { + self.pos.row = self.size.rows - 1; + } + } + + fn col_clamp(&mut self) { + if self.pos.col > self.size.cols - 1 { + self.pos.col = self.size.cols - 1; + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Size { + pub rows: u16, + pub cols: u16, +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Pos { + pub row: u16, + pub col: u16, +} diff --git a/vt100/src/lib.rs b/vt100/src/lib.rs new file mode 100644 index 000000000..922adaf5d --- /dev/null +++ b/vt100/src/lib.rs @@ -0,0 +1,61 @@ +//! This crate parses a terminal byte stream and provides an in-memory +//! representation of the rendered contents. +//! +//! # Overview +//! +//! This is essentially the terminal parser component of a graphical terminal +//! emulator pulled out into a separate crate. Although you can use this crate +//! to build a graphical terminal emulator, it also contains functionality +//! necessary for implementing terminal applications that want to run other +//! terminal applications - programs like `screen` or `tmux` for example. +//! +//! # Synopsis +//! +//! ``` +//! let mut parser = vt100::Parser::new(24, 80, 0); +//! +//! let screen = parser.screen().clone(); +//! parser.process(b"this text is \x1b[31mRED\x1b[m"); +//! assert_eq!( +//! parser.screen().cell(0, 13).unwrap().fgcolor(), +//! vt100::Color::Idx(1), +//! ); +//! +//! let screen = parser.screen().clone(); +//! parser.process(b"\x1b[3D\x1b[32mGREEN"); +//! assert_eq!( +//! parser.screen().contents_formatted(), +//! &b"\x1b[?25h\x1b[m\x1b[H\x1b[Jthis text is \x1b[32mGREEN"[..], +//! ); +//! assert_eq!( +//! parser.screen().contents_diff(&screen), +//! &b"\x1b[1;14H\x1b[32mGREEN"[..], +//! ); +//! ``` + +#![warn(clippy::pedantic)] +#![warn(clippy::as_conversions)] +#![warn(clippy::get_unwrap)] +#![allow(clippy::cognitive_complexity)] +#![allow(clippy::missing_const_for_fn)] +#![allow(clippy::similar_names)] +#![allow(clippy::struct_excessive_bools)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +#![allow(clippy::type_complexity)] + +mod attrs; +mod callbacks; +mod cell; +mod grid; +mod parser; +mod perform; +mod row; +mod screen; +mod term; + +pub use attrs::Color; +pub use callbacks::Callbacks; +pub use cell::Cell; +pub use parser::Parser; +pub use screen::{MouseProtocolEncoding, MouseProtocolMode, Screen}; diff --git a/vt100/src/parser.rs b/vt100/src/parser.rs new file mode 100644 index 000000000..2844e17f7 --- /dev/null +++ b/vt100/src/parser.rs @@ -0,0 +1,78 @@ +/// A parser for terminal output which produces an in-memory representation of +/// the terminal contents. +pub struct Parser { + parser: vte::Parser, + screen: crate::perform::WrappedScreen, +} + +impl Parser { + /// Creates a new terminal parser of the given size and with the given + /// amount of scrollback. + #[must_use] + pub fn new(rows: u16, cols: u16, scrollback_len: usize) -> Self { + Self { + parser: vte::Parser::new(), + screen: crate::perform::WrappedScreen(crate::Screen::new( + crate::grid::Size { rows, cols }, + scrollback_len, + )), + } + } + + /// Processes the contents of the given byte string, and updates the + /// in-memory terminal state. + pub fn process(&mut self, bytes: &[u8]) { + for byte in bytes { + self.parser.advance(&mut self.screen, *byte); + } + } + + /// Processes the contents of the given byte string, and updates the + /// in-memory terminal state. Calls methods on the given `Callbacks` + /// object when relevant escape sequences are seen. + pub fn process_cb( + &mut self, + bytes: &[u8], + callbacks: &mut impl crate::callbacks::Callbacks, + ) { + let mut screen = crate::perform::WrappedScreenWithCallbacks::new( + &mut self.screen, + callbacks, + ); + for byte in bytes { + self.parser.advance(&mut screen, *byte); + } + } + + /// Returns a reference to a `Screen` object containing the terminal + /// state. + #[must_use] + pub fn screen(&self) -> &crate::Screen { + &self.screen.0 + } + + /// Returns a mutable reference to a `Screen` object containing the + /// terminal state. + #[must_use] + pub fn screen_mut(&mut self) -> &mut crate::Screen { + &mut self.screen.0 + } +} + +impl Default for Parser { + /// Returns a parser with dimensions 80x24 and no scrollback. + fn default() -> Self { + Self::new(24, 80, 0) + } +} + +impl std::io::Write for Parser { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.process(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} diff --git a/vt100/src/perform.rs b/vt100/src/perform.rs new file mode 100644 index 000000000..ecf0aa436 --- /dev/null +++ b/vt100/src/perform.rs @@ -0,0 +1,305 @@ +pub struct WrappedScreen(pub crate::Screen); + +impl vte::Perform for WrappedScreen { + fn print(&mut self, c: char) { + if c == '\u{fffd}' || ('\u{80}'..'\u{a0}').contains(&c) { + log::debug!("unhandled text character: {c}"); + } + self.0.text(c); + } + + fn execute(&mut self, b: u8) { + match b { + 8 => self.0.bs(), + 9 => self.0.tab(), + 10 => self.0.lf(), + 11 => self.0.vt(), + 12 => self.0.ff(), + 13 => self.0.cr(), + // we don't implement shift in/out alternate character sets, but + // it shouldn't count as an "error" + 7 | 14 | 15 => {} + _ => { + log::debug!("unhandled control character: {b}"); + } + } + } + + fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, b: u8) { + intermediates.first().map_or_else( + || match b { + b'7' => self.0.decsc(), + b'8' => self.0.decrc(), + b'=' => self.0.deckpam(), + b'>' => self.0.deckpnm(), + b'M' => self.0.ri(), + b'c' => self.0.ris(), + b'g' => {} + _ => { + log::debug!("unhandled escape code: ESC {b}"); + } + }, + |i| { + log::debug!("unhandled escape code: ESC {i} {b}"); + }, + ); + } + + fn csi_dispatch( + &mut self, + params: &vte::Params, + intermediates: &[u8], + _ignore: bool, + c: char, + ) { + match intermediates.first() { + None => match c { + '@' => self.0.ich(canonicalize_params_1(params, 1)), + 'A' => self.0.cuu(canonicalize_params_1(params, 1)), + 'B' => self.0.cud(canonicalize_params_1(params, 1)), + 'C' => self.0.cuf(canonicalize_params_1(params, 1)), + 'D' => self.0.cub(canonicalize_params_1(params, 1)), + 'E' => self.0.cnl(canonicalize_params_1(params, 1)), + 'F' => self.0.cpl(canonicalize_params_1(params, 1)), + 'G' => self.0.cha(canonicalize_params_1(params, 1)), + 'H' => self.0.cup(canonicalize_params_2(params, 1, 1)), + 'J' => self.0.ed(canonicalize_params_1(params, 0)), + 'K' => self.0.el(canonicalize_params_1(params, 0)), + 'L' => self.0.il(canonicalize_params_1(params, 1)), + 'M' => self.0.dl(canonicalize_params_1(params, 1)), + 'P' => self.0.dch(canonicalize_params_1(params, 1)), + 'S' => self.0.su(canonicalize_params_1(params, 1)), + 'T' => self.0.sd(canonicalize_params_1(params, 1)), + 'X' => self.0.ech(canonicalize_params_1(params, 1)), + 'd' => self.0.vpa(canonicalize_params_1(params, 1)), + 'h' => self.0.sm(params), + 'l' => self.0.rm(params), + 'm' => self.0.sgr(params), + 'r' => self.0.decstbm(canonicalize_params_decstbm( + params, + self.0.grid().size(), + )), + 't' => self.0.xtwinops(params), + _ => { + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "unhandled csi sequence: CSI {} {}", + param_str(params), + c + ); + } + } + }, + Some(b'?') => match c { + 'J' => self.0.decsed(canonicalize_params_1(params, 0)), + 'K' => self.0.decsel(canonicalize_params_1(params, 0)), + 'h' => self.0.decset(params), + 'l' => self.0.decrst(params), + _ => { + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "unhandled csi sequence: CSI ? {} {}", + param_str(params), + c + ); + } + } + }, + Some(i) => { + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "unhandled csi sequence: CSI {} {} {}", + i, + param_str(params), + c + ); + } + } + } + } + + fn osc_dispatch(&mut self, params: &[&[u8]], _bel_terminated: bool) { + match (params.first(), params.get(1)) { + (Some(&b"0"), Some(s)) => self.0.osc0(s), + (Some(&b"1"), Some(s)) => self.0.osc1(s), + (Some(&b"2"), Some(s)) => self.0.osc2(s), + _ => { + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "unhandled osc sequence: OSC {}", + osc_param_str(params), + ); + } + } + } + } + + fn hook( + &mut self, + params: &vte::Params, + intermediates: &[u8], + _ignore: bool, + action: char, + ) { + if log::log_enabled!(log::Level::Debug) { + intermediates.first().map_or_else( + || { + log::debug!( + "unhandled dcs sequence: DCS {} {}", + param_str(params), + action, + ); + }, + |i| { + log::debug!( + "unhandled dcs sequence: DCS {} {} {}", + i, + param_str(params), + action, + ); + }, + ); + } + } +} + +fn canonicalize_params_1(params: &vte::Params, default: u16) -> u16 { + let first = params.iter().next().map_or(0, |x| *x.first().unwrap_or(&0)); + if first == 0 { + default + } else { + first + } +} + +fn canonicalize_params_2( + params: &vte::Params, + default1: u16, + default2: u16, +) -> (u16, u16) { + let mut iter = params.iter(); + let first = iter.next().map_or(0, |x| *x.first().unwrap_or(&0)); + let first = if first == 0 { default1 } else { first }; + + let second = iter.next().map_or(0, |x| *x.first().unwrap_or(&0)); + let second = if second == 0 { default2 } else { second }; + + (first, second) +} + +fn canonicalize_params_decstbm( + params: &vte::Params, + size: crate::grid::Size, +) -> (u16, u16) { + let mut iter = params.iter(); + let top = iter.next().map_or(0, |x| *x.first().unwrap_or(&0)); + let top = if top == 0 { 1 } else { top }; + + let bottom = iter.next().map_or(0, |x| *x.first().unwrap_or(&0)); + let bottom = if bottom == 0 { size.rows } else { bottom }; + + (top, bottom) +} + +pub fn param_str(params: &vte::Params) -> String { + let strs: Vec<_> = params + .iter() + .map(|subparams| { + let subparam_strs: Vec<_> = subparams + .iter() + .map(std::string::ToString::to_string) + .collect(); + subparam_strs.join(" : ") + }) + .collect(); + strs.join(" ; ") +} + +fn osc_param_str(params: &[&[u8]]) -> String { + let strs: Vec<_> = params + .iter() + .map(|b| format!("\"{}\"", std::string::String::from_utf8_lossy(b))) + .collect(); + strs.join(" ; ") +} + +pub struct WrappedScreenWithCallbacks<'a, T: crate::callbacks::Callbacks> { + screen: &'a mut crate::perform::WrappedScreen, + callbacks: &'a mut T, +} + +impl<'a, T: crate::callbacks::Callbacks> WrappedScreenWithCallbacks<'a, T> { + pub fn new( + screen: &'a mut crate::perform::WrappedScreen, + callbacks: &'a mut T, + ) -> Self { + Self { screen, callbacks } + } +} + +impl<'a, T: crate::callbacks::Callbacks> vte::Perform + for WrappedScreenWithCallbacks<'a, T> +{ + fn print(&mut self, c: char) { + if c == '\u{fffd}' || ('\u{80}'..'\u{a0}').contains(&c) { + self.callbacks.error(&mut self.screen.0); + } + self.screen.print(c); + } + + fn execute(&mut self, b: u8) { + match b { + 7 => self.callbacks.audible_bell(&mut self.screen.0), + 8..=15 => {} + _ => { + self.callbacks.error(&mut self.screen.0); + } + } + self.screen.execute(b); + } + + fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, b: u8) { + if intermediates.is_empty() && b == b'g' { + self.callbacks.visual_bell(&mut self.screen.0); + } + self.screen.esc_dispatch(intermediates, ignore, b); + } + + fn csi_dispatch( + &mut self, + params: &vte::Params, + intermediates: &[u8], + ignore: bool, + c: char, + ) { + if intermediates.first().is_none() && c == 't' { + let mut iter = params.iter(); + let op = iter.next().and_then(|x| x.first().copied()); + if op == Some(8) { + let (screen_rows, screen_cols) = self.screen.0.size(); + let rows = iter.next().map_or(screen_rows, |x| { + *x.first().unwrap_or(&screen_rows) + }); + let cols = iter.next().map_or(screen_cols, |x| { + *x.first().unwrap_or(&screen_cols) + }); + self.callbacks.resize(&mut self.screen.0, (rows, cols)); + } + } + self.screen.csi_dispatch(params, intermediates, ignore, c); + } + + fn osc_dispatch(&mut self, params: &[&[u8]], bel_terminated: bool) { + self.screen.osc_dispatch(params, bel_terminated); + } + + fn hook( + &mut self, + params: &vte::Params, + intermediates: &[u8], + ignore: bool, + action: char, + ) { + self.screen.hook(params, intermediates, ignore, action); + } +} diff --git a/vt100/src/row.rs b/vt100/src/row.rs new file mode 100644 index 000000000..eb56f4e3c --- /dev/null +++ b/vt100/src/row.rs @@ -0,0 +1,474 @@ +use crate::term::BufWrite as _; + +#[derive(Clone, Debug)] +pub struct Row { + cells: Vec, + wrapped: bool, +} + +impl Row { + pub fn new(cols: u16) -> Self { + Self { + cells: vec![crate::Cell::new(); usize::from(cols)], + wrapped: false, + } + } + + fn cols(&self) -> u16 { + self.cells + .len() + .try_into() + // we limit the number of cols to a u16 (see Size) + .unwrap() + } + + pub fn clear(&mut self, attrs: crate::attrs::Attrs) { + for cell in &mut self.cells { + cell.clear(attrs); + } + self.wrapped = false; + } + + fn cells(&self) -> impl Iterator { + self.cells.iter() + } + + pub fn get(&self, col: u16) -> Option<&crate::Cell> { + self.cells.get(usize::from(col)) + } + + pub fn get_mut(&mut self, col: u16) -> Option<&mut crate::Cell> { + self.cells.get_mut(usize::from(col)) + } + + pub fn insert(&mut self, i: u16, cell: crate::Cell) { + self.cells.insert(usize::from(i), cell); + self.wrapped = false; + } + + pub fn remove(&mut self, i: u16) { + self.clear_wide(i); + self.cells.remove(usize::from(i)); + self.wrapped = false; + } + + pub fn erase(&mut self, i: u16, attrs: crate::attrs::Attrs) { + let wide = self.cells[usize::from(i)].is_wide(); + self.clear_wide(i); + self.cells[usize::from(i)].clear(attrs); + if i == self.cols() - if wide { 2 } else { 1 } { + self.wrapped = false; + } + } + + pub fn truncate(&mut self, len: u16) { + self.cells.truncate(usize::from(len)); + self.wrapped = false; + let last_cell = &mut self.cells[usize::from(len) - 1]; + if last_cell.is_wide() { + last_cell.clear(*last_cell.attrs()); + } + } + + pub fn resize(&mut self, len: u16, cell: crate::Cell) { + self.cells.resize(usize::from(len), cell); + self.wrapped = false; + } + + pub fn wrap(&mut self, wrap: bool) { + self.wrapped = wrap; + } + + pub fn wrapped(&self) -> bool { + self.wrapped + } + + pub fn clear_wide(&mut self, col: u16) { + let cell = &self.cells[usize::from(col)]; + let other = if cell.is_wide() { + &mut self.cells[usize::from(col + 1)] + } else if cell.is_wide_continuation() { + &mut self.cells[usize::from(col - 1)] + } else { + return; + }; + other.clear(*other.attrs()); + } + + pub fn write_contents( + &self, + contents: &mut String, + start: u16, + width: u16, + wrapping: bool, + ) { + let mut prev_was_wide = false; + + let mut prev_col = start; + for (col, cell) in self + .cells() + .enumerate() + .skip(usize::from(start)) + .take(usize::from(width)) + { + if prev_was_wide { + prev_was_wide = false; + continue; + } + prev_was_wide = cell.is_wide(); + + // we limit the number of cols to a u16 (see Size) + let col: u16 = col.try_into().unwrap(); + if cell.has_contents() { + for _ in 0..(col - prev_col) { + contents.push(' '); + } + prev_col += col - prev_col; + + contents.push_str(&cell.contents()); + prev_col += if cell.is_wide() { 2 } else { 1 }; + } + } + if prev_col == start && wrapping { + contents.push('\n'); + } + } + + pub fn write_contents_formatted( + &self, + contents: &mut Vec, + start: u16, + width: u16, + row: u16, + wrapping: bool, + prev_pos: Option, + prev_attrs: Option, + ) -> (crate::grid::Pos, crate::attrs::Attrs) { + let mut prev_was_wide = false; + let default_cell = crate::Cell::new(); + + let mut prev_pos = prev_pos.unwrap_or_else(|| { + if wrapping { + crate::grid::Pos { + row: row - 1, + col: self.cols(), + } + } else { + crate::grid::Pos { row, col: start } + } + }); + let mut prev_attrs = prev_attrs.unwrap_or_default(); + + let first_cell = &self.cells[usize::from(start)]; + if wrapping && first_cell == &default_cell { + let default_attrs = default_cell.attrs(); + if &prev_attrs != default_attrs { + default_attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *default_attrs; + } + contents.push(b' '); + crate::term::Backspace.write_buf(contents); + crate::term::EraseChar::new(1).write_buf(contents); + prev_pos = crate::grid::Pos { row, col: 0 }; + } + + let mut erase: Option<(u16, &crate::attrs::Attrs)> = None; + for (col, cell) in self + .cells() + .enumerate() + .skip(usize::from(start)) + .take(usize::from(width)) + { + if prev_was_wide { + prev_was_wide = false; + continue; + } + prev_was_wide = cell.is_wide(); + + // we limit the number of cols to a u16 (see Size) + let col: u16 = col.try_into().unwrap(); + let pos = crate::grid::Pos { row, col }; + + if let Some((prev_col, attrs)) = erase { + if cell.has_contents() || cell.attrs() != attrs { + let new_pos = crate::grid::Pos { row, col: prev_col }; + if wrapping + && prev_pos.row + 1 == new_pos.row + && prev_pos.col >= self.cols() + { + if new_pos.col > 0 { + contents.extend( + " ".repeat(usize::from(new_pos.col)) + .as_bytes(), + ); + } else { + contents.extend(b" "); + crate::term::Backspace.write_buf(contents); + } + } else { + crate::term::MoveFromTo::new(prev_pos, new_pos) + .write_buf(contents); + } + prev_pos = new_pos; + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + crate::term::EraseChar::new(pos.col - prev_col) + .write_buf(contents); + erase = None; + } + } + + if cell != &default_cell { + let attrs = cell.attrs(); + if cell.has_contents() { + if pos != prev_pos { + if !wrapping + || prev_pos.row + 1 != pos.row + || prev_pos.col + < self.cols() - u16::from(cell.is_wide()) + || pos.col != 0 + { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + } + prev_pos = pos; + } + + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + + prev_pos.col += if cell.is_wide() { 2 } else { 1 }; + let cell_contents = cell.contents(); + contents.extend(cell_contents.as_bytes()); + } else if erase.is_none() { + erase = Some((pos.col, attrs)); + } + } + } + if let Some((prev_col, attrs)) = erase { + let new_pos = crate::grid::Pos { row, col: prev_col }; + if wrapping + && prev_pos.row + 1 == new_pos.row + && prev_pos.col >= self.cols() + { + if new_pos.col > 0 { + contents.extend( + " ".repeat(usize::from(new_pos.col)).as_bytes(), + ); + } else { + contents.extend(b" "); + crate::term::Backspace.write_buf(contents); + } + } else { + crate::term::MoveFromTo::new(prev_pos, new_pos) + .write_buf(contents); + } + prev_pos = new_pos; + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + crate::term::ClearRowForward.write_buf(contents); + } + + (prev_pos, prev_attrs) + } + + // while it's true that most of the logic in this is identical to + // write_contents_formatted, i can't figure out how to break out the + // common parts without making things noticeably slower. + pub fn write_contents_diff( + &self, + contents: &mut Vec, + prev: &Self, + start: u16, + width: u16, + row: u16, + wrapping: bool, + prev_wrapping: bool, + mut prev_pos: crate::grid::Pos, + mut prev_attrs: crate::attrs::Attrs, + ) -> (crate::grid::Pos, crate::attrs::Attrs) { + let mut prev_was_wide = false; + + let first_cell = &self.cells[usize::from(start)]; + let prev_first_cell = &prev.cells[usize::from(start)]; + if wrapping + && !prev_wrapping + && first_cell == prev_first_cell + && prev_pos.row + 1 == row + && prev_pos.col + >= self.cols() - u16::from(prev_first_cell.is_wide()) + { + let first_cell_attrs = first_cell.attrs(); + if &prev_attrs != first_cell_attrs { + first_cell_attrs + .write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *first_cell_attrs; + } + let mut cell_contents = prev_first_cell.contents(); + let need_erase = if cell_contents.is_empty() { + cell_contents = " ".to_string(); + true + } else { + false + }; + contents.extend(cell_contents.as_bytes()); + crate::term::Backspace.write_buf(contents); + if prev_first_cell.is_wide() { + crate::term::Backspace.write_buf(contents); + } + if need_erase { + crate::term::EraseChar::new(1).write_buf(contents); + } + prev_pos = crate::grid::Pos { row, col: 0 }; + } + + let mut erase: Option<(u16, &crate::attrs::Attrs)> = None; + for (col, (cell, prev_cell)) in self + .cells() + .zip(prev.cells()) + .enumerate() + .skip(usize::from(start)) + .take(usize::from(width)) + { + if prev_was_wide { + prev_was_wide = false; + continue; + } + prev_was_wide = cell.is_wide(); + + // we limit the number of cols to a u16 (see Size) + let col: u16 = col.try_into().unwrap(); + let pos = crate::grid::Pos { row, col }; + + if let Some((prev_col, attrs)) = erase { + if cell.has_contents() || cell.attrs() != attrs { + let new_pos = crate::grid::Pos { row, col: prev_col }; + if wrapping + && prev_pos.row + 1 == new_pos.row + && prev_pos.col >= self.cols() + { + if new_pos.col > 0 { + contents.extend( + " ".repeat(usize::from(new_pos.col)) + .as_bytes(), + ); + } else { + contents.extend(b" "); + crate::term::Backspace.write_buf(contents); + } + } else { + crate::term::MoveFromTo::new(prev_pos, new_pos) + .write_buf(contents); + } + prev_pos = new_pos; + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + crate::term::EraseChar::new(pos.col - prev_col) + .write_buf(contents); + erase = None; + } + } + + if cell != prev_cell { + let attrs = cell.attrs(); + if cell.has_contents() { + if pos != prev_pos { + if !wrapping + || prev_pos.row + 1 != pos.row + || prev_pos.col + < self.cols() - u16::from(cell.is_wide()) + || pos.col != 0 + { + crate::term::MoveFromTo::new(prev_pos, pos) + .write_buf(contents); + } + prev_pos = pos; + } + + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + + prev_pos.col += if cell.is_wide() { 2 } else { 1 }; + contents.extend(cell.contents().as_bytes()); + } else if erase.is_none() { + erase = Some((pos.col, attrs)); + } + } + } + if let Some((prev_col, attrs)) = erase { + let new_pos = crate::grid::Pos { row, col: prev_col }; + if wrapping + && prev_pos.row + 1 == new_pos.row + && prev_pos.col >= self.cols() + { + if new_pos.col > 0 { + contents.extend( + " ".repeat(usize::from(new_pos.col)).as_bytes(), + ); + } else { + contents.extend(b" "); + crate::term::Backspace.write_buf(contents); + } + } else { + crate::term::MoveFromTo::new(prev_pos, new_pos) + .write_buf(contents); + } + prev_pos = new_pos; + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + crate::term::ClearRowForward.write_buf(contents); + } + + // if this row is going from wrapped to not wrapped, we need to erase + // and redraw the last character to break wrapping. if this row is + // wrapped, we need to redraw the last character without erasing it to + // position the cursor after the end of the line correctly so that + // drawing the next line can just start writing and be wrapped. + if (!self.wrapped && prev.wrapped) || (!prev.wrapped && self.wrapped) + { + let end_pos = if self.cells[usize::from(self.cols() - 1)] + .is_wide_continuation() + { + crate::grid::Pos { + row, + col: self.cols() - 2, + } + } else { + crate::grid::Pos { + row, + col: self.cols() - 1, + } + }; + crate::term::MoveFromTo::new(prev_pos, end_pos) + .write_buf(contents); + prev_pos = end_pos; + if !self.wrapped { + crate::term::EraseChar::new(1).write_buf(contents); + } + let end_cell = &self.cells[usize::from(end_pos.col)]; + if end_cell.has_contents() { + let attrs = end_cell.attrs(); + if &prev_attrs != attrs { + attrs.write_escape_code_diff(contents, &prev_attrs); + prev_attrs = *attrs; + } + contents.extend(end_cell.contents().as_bytes()); + prev_pos.col += if end_cell.is_wide() { 2 } else { 1 }; + } + } + + (prev_pos, prev_attrs) + } +} diff --git a/vt100/src/screen.rs b/vt100/src/screen.rs new file mode 100644 index 000000000..0d3205126 --- /dev/null +++ b/vt100/src/screen.rs @@ -0,0 +1,1511 @@ +use crate::term::BufWrite as _; +use unicode_width::UnicodeWidthChar as _; + +const MODE_APPLICATION_KEYPAD: u8 = 0b0000_0001; +const MODE_APPLICATION_CURSOR: u8 = 0b0000_0010; +const MODE_HIDE_CURSOR: u8 = 0b0000_0100; +const MODE_ALTERNATE_SCREEN: u8 = 0b0000_1000; +const MODE_BRACKETED_PASTE: u8 = 0b0001_0000; + +/// The xterm mouse handling mode currently in use. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum MouseProtocolMode { + /// Mouse handling is disabled. + None, + + /// Mouse button events should be reported on button press. Also known as + /// X10 mouse mode. + Press, + + /// Mouse button events should be reported on button press and release. + /// Also known as VT200 mouse mode. + PressRelease, + + // Highlight, + /// Mouse button events should be reported on button press and release, as + /// well as when the mouse moves between cells while a button is held + /// down. + ButtonMotion, + + /// Mouse button events should be reported on button press and release, + /// and mouse motion events should be reported when the mouse moves + /// between cells regardless of whether a button is held down or not. + AnyMotion, + // DecLocator, +} + +impl Default for MouseProtocolMode { + fn default() -> Self { + Self::None + } +} + +/// The encoding to use for the enabled `MouseProtocolMode`. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum MouseProtocolEncoding { + /// Default single-printable-byte encoding. + Default, + + /// UTF-8-based encoding. + Utf8, + + /// SGR-like encoding. + Sgr, + // Urxvt, +} + +impl Default for MouseProtocolEncoding { + fn default() -> Self { + Self::Default + } +} + +/// Represents the overall terminal state. +#[derive(Clone, Debug)] +pub struct Screen { + grid: crate::grid::Grid, + alternate_grid: crate::grid::Grid, + + attrs: crate::attrs::Attrs, + saved_attrs: crate::attrs::Attrs, + + title: String, + icon_name: String, + + modes: u8, + mouse_protocol_mode: MouseProtocolMode, + mouse_protocol_encoding: MouseProtocolEncoding, +} + +impl Screen { + pub(crate) fn new( + size: crate::grid::Size, + scrollback_len: usize, + ) -> Self { + let mut grid = crate::grid::Grid::new(size, scrollback_len); + grid.allocate_rows(); + Self { + grid, + alternate_grid: crate::grid::Grid::new(size, 0), + + attrs: crate::attrs::Attrs::default(), + saved_attrs: crate::attrs::Attrs::default(), + + title: String::default(), + icon_name: String::default(), + + modes: 0, + mouse_protocol_mode: MouseProtocolMode::default(), + mouse_protocol_encoding: MouseProtocolEncoding::default(), + } + } + + /// Resizes the terminal. + pub fn set_size(&mut self, rows: u16, cols: u16) { + self.grid.set_size(crate::grid::Size { rows, cols }); + self.alternate_grid + .set_size(crate::grid::Size { rows, cols }); + } + + /// Returns the current size of the terminal. + /// + /// The return value will be (rows, cols). + #[must_use] + pub fn size(&self) -> (u16, u16) { + let size = self.grid().size(); + (size.rows, size.cols) + } + + /// Scrolls to the given position in the scrollback. + /// + /// This position indicates the offset from the top of the screen, and + /// should be `0` to put the normal screen in view. + /// + /// This affects the return values of methods called on the screen: for + /// instance, `screen.cell(0, 0)` will return the top left corner of the + /// screen after taking the scrollback offset into account. + /// + /// The value given will be clamped to the actual size of the scrollback. + pub fn set_scrollback(&mut self, rows: usize) { + self.grid_mut().set_scrollback(rows); + } + + /// Returns the current position in the scrollback. + /// + /// This position indicates the offset from the top of the screen, and is + /// `0` when the normal screen is in view. + #[must_use] + pub fn scrollback(&self) -> usize { + self.grid().scrollback() + } + + /// Returns the text contents of the terminal. + /// + /// This will not include any formatting information, and will be in plain + /// text format. + #[must_use] + pub fn contents(&self) -> String { + let mut contents = String::new(); + self.write_contents(&mut contents); + contents + } + + fn write_contents(&self, contents: &mut String) { + self.grid().write_contents(contents); + } + + /// Returns the text contents of the terminal by row, restricted to the + /// given subset of columns. + /// + /// This will not include any formatting information, and will be in plain + /// text format. + /// + /// Newlines will not be included. + pub fn rows( + &self, + start: u16, + width: u16, + ) -> impl Iterator + '_ { + self.grid().visible_rows().map(move |row| { + let mut contents = String::new(); + row.write_contents(&mut contents, start, width, false); + contents + }) + } + + /// Returns the text contents of the terminal logically between two cells. + /// This will include the remainder of the starting row after `start_col`, + /// followed by the entire contents of the rows between `start_row` and + /// `end_row`, followed by the beginning of the `end_row` up until + /// `end_col`. This is useful for things like determining the contents of + /// a clipboard selection. + #[must_use] + pub fn contents_between( + &self, + start_row: u16, + start_col: u16, + end_row: u16, + end_col: u16, + ) -> String { + match start_row.cmp(&end_row) { + std::cmp::Ordering::Less => { + let (_, cols) = self.size(); + let mut contents = String::new(); + for (i, row) in self + .grid() + .visible_rows() + .enumerate() + .skip(usize::from(start_row)) + .take(usize::from(end_row) - usize::from(start_row) + 1) + { + if i == usize::from(start_row) { + row.write_contents( + &mut contents, + start_col, + cols - start_col, + false, + ); + if !row.wrapped() { + contents.push('\n'); + } + } else if i == usize::from(end_row) { + row.write_contents(&mut contents, 0, end_col, false); + } else { + row.write_contents(&mut contents, 0, cols, false); + if !row.wrapped() { + contents.push('\n'); + } + } + } + contents + } + std::cmp::Ordering::Equal => { + if start_col < end_col { + self.rows(start_col, end_col - start_col) + .nth(usize::from(start_row)) + .unwrap_or_default() + } else { + String::new() + } + } + std::cmp::Ordering::Greater => String::new(), + } + } + + /// Return escape codes sufficient to reproduce the entire contents of the + /// current terminal state. This is a convenience wrapper around + /// `contents_formatted`, `input_mode_formatted`, and `title_formatted`. + #[must_use] + pub fn state_formatted(&self) -> Vec { + let mut contents = vec![]; + self.write_contents_formatted(&mut contents); + self.write_input_mode_formatted(&mut contents); + self.write_title_formatted(&mut contents); + contents + } + + /// Return escape codes sufficient to turn the terminal state of the + /// screen `prev` into the current terminal state. This is a convenience + /// wrapper around `contents_diff`, `input_mode_diff`, and `title_diff` + #[must_use] + pub fn state_diff(&self, prev: &Self) -> Vec { + let mut contents = vec![]; + self.write_contents_diff(&mut contents, prev); + self.write_input_mode_diff(&mut contents, prev); + self.write_title_diff(&mut contents, prev); + contents + } + + /// Returns the formatted visible contents of the terminal. + /// + /// Formatting information will be included inline as terminal escape + /// codes. The result will be suitable for feeding directly to a raw + /// terminal parser, and will result in the same visual output. + #[must_use] + pub fn contents_formatted(&self) -> Vec { + let mut contents = vec![]; + self.write_contents_formatted(&mut contents); + contents + } + + fn write_contents_formatted(&self, contents: &mut Vec) { + crate::term::HideCursor::new(self.hide_cursor()).write_buf(contents); + let prev_attrs = self.grid().write_contents_formatted(contents); + self.attrs.write_escape_code_diff(contents, &prev_attrs); + } + + /// Returns the formatted visible contents of the terminal by row, + /// restricted to the given subset of columns. + /// + /// Formatting information will be included inline as terminal escape + /// codes. The result will be suitable for feeding directly to a raw + /// terminal parser, and will result in the same visual output. + /// + /// You are responsible for positioning the cursor before printing each + /// row, and the final cursor position after displaying each row is + /// unspecified. + // the unwraps in this method shouldn't be reachable + #[allow(clippy::missing_panics_doc)] + pub fn rows_formatted( + &self, + start: u16, + width: u16, + ) -> impl Iterator> + '_ { + let mut wrapping = false; + self.grid().visible_rows().enumerate().map(move |(i, row)| { + // number of rows in a grid is stored in a u16 (see Size), so + // visible_rows can never return enough rows to overflow here + let i = i.try_into().unwrap(); + let mut contents = vec![]; + row.write_contents_formatted( + &mut contents, + start, + width, + i, + wrapping, + None, + None, + ); + if start == 0 && width == self.grid.size().cols { + wrapping = row.wrapped(); + } + contents + }) + } + + /// Returns a terminal byte stream sufficient to turn the visible contents + /// of the screen described by `prev` into the visible contents of the + /// screen described by `self`. + /// + /// The result of rendering `prev.contents_formatted()` followed by + /// `self.contents_diff(prev)` should be equivalent to the result of + /// rendering `self.contents_formatted()`. This is primarily useful when + /// you already have a terminal parser whose state is described by `prev`, + /// since the diff will likely require less memory and cause less + /// flickering than redrawing the entire screen contents. + #[must_use] + pub fn contents_diff(&self, prev: &Self) -> Vec { + let mut contents = vec![]; + self.write_contents_diff(&mut contents, prev); + contents + } + + fn write_contents_diff(&self, contents: &mut Vec, prev: &Self) { + if self.hide_cursor() != prev.hide_cursor() { + crate::term::HideCursor::new(self.hide_cursor()) + .write_buf(contents); + } + let prev_attrs = self.grid().write_contents_diff( + contents, + prev.grid(), + prev.attrs, + ); + self.attrs.write_escape_code_diff(contents, &prev_attrs); + } + + /// Returns a sequence of terminal byte streams sufficient to turn the + /// visible contents of the subset of each row from `prev` (as described + /// by `start` and `width`) into the visible contents of the corresponding + /// row subset in `self`. + /// + /// You are responsible for positioning the cursor before printing each + /// row, and the final cursor position after displaying each row is + /// unspecified. + // the unwraps in this method shouldn't be reachable + #[allow(clippy::missing_panics_doc)] + pub fn rows_diff<'a>( + &'a self, + prev: &'a Self, + start: u16, + width: u16, + ) -> impl Iterator> + 'a { + self.grid() + .visible_rows() + .zip(prev.grid().visible_rows()) + .enumerate() + .map(move |(i, (row, prev_row))| { + // number of rows in a grid is stored in a u16 (see Size), so + // visible_rows can never return enough rows to overflow here + let i = i.try_into().unwrap(); + let mut contents = vec![]; + row.write_contents_diff( + &mut contents, + prev_row, + start, + width, + i, + false, + false, + crate::grid::Pos { row: i, col: start }, + crate::attrs::Attrs::default(), + ); + contents + }) + } + + /// Returns terminal escape sequences sufficient to set the current + /// terminal's input modes. + /// + /// Supported modes are: + /// * application keypad + /// * application cursor + /// * bracketed paste + /// * xterm mouse support + #[must_use] + pub fn input_mode_formatted(&self) -> Vec { + let mut contents = vec![]; + self.write_input_mode_formatted(&mut contents); + contents + } + + fn write_input_mode_formatted(&self, contents: &mut Vec) { + crate::term::ApplicationKeypad::new( + self.mode(MODE_APPLICATION_KEYPAD), + ) + .write_buf(contents); + crate::term::ApplicationCursor::new( + self.mode(MODE_APPLICATION_CURSOR), + ) + .write_buf(contents); + crate::term::BracketedPaste::new(self.mode(MODE_BRACKETED_PASTE)) + .write_buf(contents); + crate::term::MouseProtocolMode::new( + self.mouse_protocol_mode, + MouseProtocolMode::None, + ) + .write_buf(contents); + crate::term::MouseProtocolEncoding::new( + self.mouse_protocol_encoding, + MouseProtocolEncoding::Default, + ) + .write_buf(contents); + } + + /// Returns terminal escape sequences sufficient to change the previous + /// terminal's input modes to the input modes enabled in the current + /// terminal. + #[must_use] + pub fn input_mode_diff(&self, prev: &Self) -> Vec { + let mut contents = vec![]; + self.write_input_mode_diff(&mut contents, prev); + contents + } + + fn write_input_mode_diff(&self, contents: &mut Vec, prev: &Self) { + if self.mode(MODE_APPLICATION_KEYPAD) + != prev.mode(MODE_APPLICATION_KEYPAD) + { + crate::term::ApplicationKeypad::new( + self.mode(MODE_APPLICATION_KEYPAD), + ) + .write_buf(contents); + } + if self.mode(MODE_APPLICATION_CURSOR) + != prev.mode(MODE_APPLICATION_CURSOR) + { + crate::term::ApplicationCursor::new( + self.mode(MODE_APPLICATION_CURSOR), + ) + .write_buf(contents); + } + if self.mode(MODE_BRACKETED_PASTE) != prev.mode(MODE_BRACKETED_PASTE) + { + crate::term::BracketedPaste::new(self.mode(MODE_BRACKETED_PASTE)) + .write_buf(contents); + } + crate::term::MouseProtocolMode::new( + self.mouse_protocol_mode, + prev.mouse_protocol_mode, + ) + .write_buf(contents); + crate::term::MouseProtocolEncoding::new( + self.mouse_protocol_encoding, + prev.mouse_protocol_encoding, + ) + .write_buf(contents); + } + + /// Returns terminal escape sequences sufficient to set the current + /// terminal's window title. + #[must_use] + pub fn title_formatted(&self) -> Vec { + let mut contents = vec![]; + self.write_title_formatted(&mut contents); + contents + } + + fn write_title_formatted(&self, contents: &mut Vec) { + crate::term::ChangeTitle::new(&self.icon_name, &self.title, "", "") + .write_buf(contents); + } + + /// Returns terminal escape sequences sufficient to change the previous + /// terminal's window title to the window title set in the current + /// terminal. + #[must_use] + pub fn title_diff(&self, prev: &Self) -> Vec { + let mut contents = vec![]; + self.write_title_diff(&mut contents, prev); + contents + } + + fn write_title_diff(&self, contents: &mut Vec, prev: &Self) { + crate::term::ChangeTitle::new( + &self.icon_name, + &self.title, + &prev.icon_name, + &prev.title, + ) + .write_buf(contents); + } + + /// Returns terminal escape sequences sufficient to set the current + /// terminal's drawing attributes. + /// + /// Supported drawing attributes are: + /// * fgcolor + /// * bgcolor + /// * bold + /// * italic + /// * underline + /// * inverse + /// + /// This is not typically necessary, since `contents_formatted` will leave + /// the current active drawing attributes in the correct state, but this + /// can be useful in the case of drawing additional things on top of a + /// terminal output, since you will need to restore the terminal state + /// without the terminal contents necessarily being the same. + #[must_use] + pub fn attributes_formatted(&self) -> Vec { + let mut contents = vec![]; + self.write_attributes_formatted(&mut contents); + contents + } + + fn write_attributes_formatted(&self, contents: &mut Vec) { + crate::term::ClearAttrs.write_buf(contents); + self.attrs.write_escape_code_diff( + contents, + &crate::attrs::Attrs::default(), + ); + } + + /// Returns the current cursor position of the terminal. + /// + /// The return value will be (row, col). + #[must_use] + pub fn cursor_position(&self) -> (u16, u16) { + let pos = self.grid().pos(); + (pos.row, pos.col) + } + + /// Returns terminal escape sequences sufficient to set the current + /// cursor state of the terminal. + /// + /// This is not typically necessary, since `contents_formatted` will leave + /// the cursor in the correct state, but this can be useful in the case of + /// drawing additional things on top of a terminal output, since you will + /// need to restore the terminal state without the terminal contents + /// necessarily being the same. + /// + /// Note that the bytes returned by this function may alter the active + /// drawing attributes, because it may require redrawing existing cells in + /// order to position the cursor correctly (for instance, in the case + /// where the cursor is past the end of a row). Therefore, you should + /// ensure to reset the active drawing attributes if necessary after + /// processing this data, for instance by using `attributes_formatted`. + #[must_use] + pub fn cursor_state_formatted(&self) -> Vec { + let mut contents = vec![]; + self.write_cursor_state_formatted(&mut contents); + contents + } + + fn write_cursor_state_formatted(&self, contents: &mut Vec) { + crate::term::HideCursor::new(self.hide_cursor()).write_buf(contents); + self.grid() + .write_cursor_position_formatted(contents, None, None); + + // we don't just call write_attributes_formatted here, because that + // would still be confusing - consider the case where the user sets + // their own unrelated drawing attributes (on a different parser + // instance) and then calls cursor_state_formatted. just documenting + // it and letting the user handle it on their own is more + // straightforward. + } + + /// Returns the `Cell` object at the given location in the terminal, if it + /// exists. + #[must_use] + pub fn cell(&self, row: u16, col: u16) -> Option<&crate::Cell> { + self.grid().visible_cell(crate::grid::Pos { row, col }) + } + + /// Returns whether the text in row `row` should wrap to the next line. + #[must_use] + pub fn row_wrapped(&self, row: u16) -> bool { + self.grid() + .visible_row(row) + .map_or(false, crate::row::Row::wrapped) + } + + /// Returns the terminal's window title. + #[must_use] + pub fn title(&self) -> &str { + &self.title + } + + /// Returns the terminal's icon name. + #[must_use] + pub fn icon_name(&self) -> &str { + &self.icon_name + } + + /// Returns whether the alternate screen is currently in use. + #[must_use] + pub fn alternate_screen(&self) -> bool { + self.mode(MODE_ALTERNATE_SCREEN) + } + + /// Returns whether the terminal should be in application keypad mode. + #[must_use] + pub fn application_keypad(&self) -> bool { + self.mode(MODE_APPLICATION_KEYPAD) + } + + /// Returns whether the terminal should be in application cursor mode. + #[must_use] + pub fn application_cursor(&self) -> bool { + self.mode(MODE_APPLICATION_CURSOR) + } + + /// Returns whether the terminal should be in hide cursor mode. + #[must_use] + pub fn hide_cursor(&self) -> bool { + self.mode(MODE_HIDE_CURSOR) + } + + /// Returns whether the terminal should be in bracketed paste mode. + #[must_use] + pub fn bracketed_paste(&self) -> bool { + self.mode(MODE_BRACKETED_PASTE) + } + + /// Returns the currently active `MouseProtocolMode` + #[must_use] + pub fn mouse_protocol_mode(&self) -> MouseProtocolMode { + self.mouse_protocol_mode + } + + /// Returns the currently active `MouseProtocolEncoding` + #[must_use] + pub fn mouse_protocol_encoding(&self) -> MouseProtocolEncoding { + self.mouse_protocol_encoding + } + + /// Returns the currently active foreground color. + #[must_use] + pub fn fgcolor(&self) -> crate::Color { + self.attrs.fgcolor + } + + /// Returns the currently active background color. + #[must_use] + pub fn bgcolor(&self) -> crate::Color { + self.attrs.bgcolor + } + + /// Returns whether newly drawn text should be rendered with the bold text + /// attribute. + #[must_use] + pub fn bold(&self) -> bool { + self.attrs.bold() + } + + /// Returns whether newly drawn text should be rendered with the italic + /// text attribute. + #[must_use] + pub fn italic(&self) -> bool { + self.attrs.italic() + } + + /// Returns whether newly drawn text should be rendered with the + /// underlined text attribute. + #[must_use] + pub fn underline(&self) -> bool { + self.attrs.underline() + } + + /// Returns whether newly drawn text should be rendered with the inverse + /// text attribute. + #[must_use] + pub fn inverse(&self) -> bool { + self.attrs.inverse() + } + + pub(crate) fn grid(&self) -> &crate::grid::Grid { + if self.mode(MODE_ALTERNATE_SCREEN) { + &self.alternate_grid + } else { + &self.grid + } + } + + fn grid_mut(&mut self) -> &mut crate::grid::Grid { + if self.mode(MODE_ALTERNATE_SCREEN) { + &mut self.alternate_grid + } else { + &mut self.grid + } + } + + fn enter_alternate_grid(&mut self) { + self.grid_mut().set_scrollback(0); + self.set_mode(MODE_ALTERNATE_SCREEN); + self.alternate_grid.allocate_rows(); + } + + fn exit_alternate_grid(&mut self) { + self.clear_mode(MODE_ALTERNATE_SCREEN); + } + + fn save_cursor(&mut self) { + self.grid_mut().save_cursor(); + self.saved_attrs = self.attrs; + } + + fn restore_cursor(&mut self) { + self.grid_mut().restore_cursor(); + self.attrs = self.saved_attrs; + } + + fn set_mode(&mut self, mode: u8) { + self.modes |= mode; + } + + fn clear_mode(&mut self, mode: u8) { + self.modes &= !mode; + } + + fn mode(&self, mode: u8) -> bool { + self.modes & mode != 0 + } + + fn set_mouse_mode(&mut self, mode: MouseProtocolMode) { + self.mouse_protocol_mode = mode; + } + + fn clear_mouse_mode(&mut self, mode: MouseProtocolMode) { + if self.mouse_protocol_mode == mode { + self.mouse_protocol_mode = MouseProtocolMode::default(); + } + } + + fn set_mouse_encoding(&mut self, encoding: MouseProtocolEncoding) { + self.mouse_protocol_encoding = encoding; + } + + fn clear_mouse_encoding(&mut self, encoding: MouseProtocolEncoding) { + if self.mouse_protocol_encoding == encoding { + self.mouse_protocol_encoding = MouseProtocolEncoding::default(); + } + } +} + +impl Screen { + pub(crate) fn text(&mut self, c: char) { + let pos = self.grid().pos(); + let size = self.grid().size(); + let attrs = self.attrs; + + let width = c.width(); + if width.is_none() && (u32::from(c)) < 256 { + // don't even try to draw control characters + return; + } + let width = width + .unwrap_or(1) + .try_into() + // width() can only return 0, 1, or 2 + .unwrap(); + + // it doesn't make any sense to wrap if the last column in a row + // didn't already have contents. don't try to handle the case where a + // character wraps because there was only one column left in the + // previous row - literally everything handles this case differently, + // and this is tmux behavior (and also the simplest). i'm open to + // reconsidering this behavior, but only with a really good reason + // (xterm handles this by introducing the concept of triple width + // cells, which i really don't want to do). + let mut wrap = false; + if pos.col > size.cols - width { + let last_cell = self + .grid() + .drawing_cell(crate::grid::Pos { + row: pos.row, + col: size.cols - 1, + }) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always have a valid + // row value. size.cols - 1 is also always a valid column. + .unwrap(); + if last_cell.has_contents() || last_cell.is_wide_continuation() { + wrap = true; + } + } + self.grid_mut().col_wrap(width, wrap); + let pos = self.grid().pos(); + + if width == 0 { + if pos.col > 0 { + let mut prev_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row, + col: pos.col - 1, + }) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always have a + // valid row value. pos.col - 1 is valid because we just + // checked for pos.col > 0. + .unwrap(); + if prev_cell.is_wide_continuation() { + prev_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row, + col: pos.col - 2, + }) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always have a + // valid row value. we know pos.col - 2 is valid + // because the cell at pos.col - 1 is a wide + // continuation character, which means there must be + // the first half of the wide character before it. + .unwrap(); + } + prev_cell.append(c); + } else if pos.row > 0 { + let prev_row = self + .grid() + .drawing_row(pos.row - 1) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always have a + // valid row value. pos.row - 1 is valid because we just + // checked for pos.row > 0. + .unwrap(); + if prev_row.wrapped() { + let mut prev_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row - 1, + col: size.cols - 1, + }) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always have a + // valid row value. pos.row - 1 is valid because we + // just checked for pos.row > 0. col of size.cols - 1 + // is always valid. + .unwrap(); + if prev_cell.is_wide_continuation() { + prev_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row - 1, + col: size.cols - 2, + }) + // pos.row is valid, since it comes directly from + // self.grid().pos() which we assume to always + // have a valid row value. pos.row - 1 is valid + // because we just checked for pos.row > 0. col of + // size.cols - 2 is valid because the cell at + // size.cols - 1 is a wide continuation character, + // so it must have the first half of the wide + // character before it. + .unwrap(); + } + prev_cell.append(c); + } + } + } else { + if self + .grid() + .drawing_cell(pos) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because we + // called col_wrap() immediately before this, which ensures + // that self.grid().pos().col has a valid value. + .unwrap() + .is_wide_continuation() + { + let prev_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row, + col: pos.col - 1, + }) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because + // we called col_wrap() immediately before this, which + // ensures that self.grid().pos().col has a valid value. + // pos.col - 1 is valid because the cell at pos.col is a + // wide continuation character, so it must have the first + // half of the wide character before it. + .unwrap(); + prev_cell.clear(attrs); + } + + if self + .grid() + .drawing_cell(pos) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because we + // called col_wrap() immediately before this, which ensures + // that self.grid().pos().col has a valid value. + .unwrap() + .is_wide() + { + let next_cell = self + .grid_mut() + .drawing_cell_mut(crate::grid::Pos { + row: pos.row, + col: pos.col + 1, + }) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because + // we called col_wrap() immediately before this, which + // ensures that self.grid().pos().col has a valid value. + // pos.col + 1 is valid because the cell at pos.col is a + // wide character, so it must have the second half of the + // wide character after it. + .unwrap(); + next_cell.set(' ', attrs); + } + + let cell = self + .grid_mut() + .drawing_cell_mut(pos) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because we + // called col_wrap() immediately before this, which ensures + // that self.grid().pos().col has a valid value. + .unwrap(); + cell.set(c, attrs); + self.grid_mut().col_inc(1); + if width > 1 { + let pos = self.grid().pos(); + if self + .grid() + .drawing_cell(pos) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because + // we called col_wrap() earlier, which ensures that + // self.grid().pos().col has a valid value. this is true + // even though we just called col_inc, because this branch + // only happens if width > 1, and col_wrap takes width + // into account. + .unwrap() + .is_wide() + { + let next_next_pos = crate::grid::Pos { + row: pos.row, + col: pos.col + 1, + }; + let next_next_cell = self + .grid_mut() + .drawing_cell_mut(next_next_pos) + // pos.row is valid because we assume + // self.grid().pos() to always have a valid row value. + // pos.col is valid because we called col_wrap() + // earlier, which ensures that self.grid().pos().col + // has a valid value. this is true even though we just + // called col_inc, because this branch only happens if + // width > 1, and col_wrap takes width into account. + // pos.col + 1 is valid because the cell at pos.col is + // wide, and so it must have the second half of the + // wide character after it. + .unwrap(); + next_next_cell.clear(attrs); + if next_next_pos.col == size.cols - 1 { + self.grid_mut() + .drawing_row_mut(pos.row) + // we assume self.grid().pos().row is always valid + .unwrap() + .wrap(false); + } + } + let next_cell = self + .grid_mut() + .drawing_cell_mut(pos) + // pos.row is valid because we assume self.grid().pos() to + // always have a valid row value. pos.col is valid because + // we called col_wrap() earlier, which ensures that + // self.grid().pos().col has a valid value. this is true + // even though we just called col_inc, because this branch + // only happens if width > 1, and col_wrap takes width + // into account. + .unwrap(); + next_cell.clear(crate::attrs::Attrs::default()); + next_cell.set_wide_continuation(true); + self.grid_mut().col_inc(1); + } + } + } + + // control codes + + pub(crate) fn bs(&mut self) { + self.grid_mut().col_dec(1); + } + + pub(crate) fn tab(&mut self) { + self.grid_mut().col_tab(); + } + + pub(crate) fn lf(&mut self) { + self.grid_mut().row_inc_scroll(1); + } + + pub(crate) fn vt(&mut self) { + self.lf(); + } + + pub(crate) fn ff(&mut self) { + self.lf(); + } + + pub(crate) fn cr(&mut self) { + self.grid_mut().col_set(0); + } + + // escape codes + + // ESC 7 + pub(crate) fn decsc(&mut self) { + self.save_cursor(); + } + + // ESC 8 + pub(crate) fn decrc(&mut self) { + self.restore_cursor(); + } + + // ESC = + pub(crate) fn deckpam(&mut self) { + self.set_mode(MODE_APPLICATION_KEYPAD); + } + + // ESC > + pub(crate) fn deckpnm(&mut self) { + self.clear_mode(MODE_APPLICATION_KEYPAD); + } + + // ESC M + pub(crate) fn ri(&mut self) { + self.grid_mut().row_dec_scroll(1); + } + + // ESC c + pub(crate) fn ris(&mut self) { + let title = self.title.clone(); + let icon_name = self.icon_name.clone(); + + *self = Self::new(self.grid.size(), self.grid.scrollback_len()); + + self.title = title; + self.icon_name = icon_name; + } + + // csi codes + + // CSI @ + pub(crate) fn ich(&mut self, count: u16) { + self.grid_mut().insert_cells(count); + } + + // CSI A + pub(crate) fn cuu(&mut self, offset: u16) { + self.grid_mut().row_dec_clamp(offset); + } + + // CSI B + pub(crate) fn cud(&mut self, offset: u16) { + self.grid_mut().row_inc_clamp(offset); + } + + // CSI C + pub(crate) fn cuf(&mut self, offset: u16) { + self.grid_mut().col_inc_clamp(offset); + } + + // CSI D + pub(crate) fn cub(&mut self, offset: u16) { + self.grid_mut().col_dec(offset); + } + + // CSI E + pub(crate) fn cnl(&mut self, offset: u16) { + self.grid_mut().col_set(0); + self.grid_mut().row_inc_clamp(offset); + } + + // CSI F + pub(crate) fn cpl(&mut self, offset: u16) { + self.grid_mut().col_set(0); + self.grid_mut().row_dec_clamp(offset); + } + + // CSI G + pub(crate) fn cha(&mut self, col: u16) { + self.grid_mut().col_set(col - 1); + } + + // CSI H + pub(crate) fn cup(&mut self, (row, col): (u16, u16)) { + self.grid_mut().set_pos(crate::grid::Pos { + row: row - 1, + col: col - 1, + }); + } + + // CSI J + pub(crate) fn ed(&mut self, mode: u16) { + let attrs = self.attrs; + match mode { + 0 => self.grid_mut().erase_all_forward(attrs), + 1 => self.grid_mut().erase_all_backward(attrs), + 2 => self.grid_mut().erase_all(attrs), + n => { + log::debug!("unhandled ED mode: {n}"); + } + } + } + + // CSI ? J + pub(crate) fn decsed(&mut self, mode: u16) { + self.ed(mode); + } + + // CSI K + pub(crate) fn el(&mut self, mode: u16) { + let attrs = self.attrs; + match mode { + 0 => self.grid_mut().erase_row_forward(attrs), + 1 => self.grid_mut().erase_row_backward(attrs), + 2 => self.grid_mut().erase_row(attrs), + n => { + log::debug!("unhandled EL mode: {n}"); + } + } + } + + // CSI ? K + pub(crate) fn decsel(&mut self, mode: u16) { + self.el(mode); + } + + // CSI L + pub(crate) fn il(&mut self, count: u16) { + self.grid_mut().insert_lines(count); + } + + // CSI M + pub(crate) fn dl(&mut self, count: u16) { + self.grid_mut().delete_lines(count); + } + + // CSI P + pub(crate) fn dch(&mut self, count: u16) { + self.grid_mut().delete_cells(count); + } + + // CSI S + pub(crate) fn su(&mut self, count: u16) { + self.grid_mut().scroll_up(count); + } + + // CSI T + pub(crate) fn sd(&mut self, count: u16) { + self.grid_mut().scroll_down(count); + } + + // CSI X + pub(crate) fn ech(&mut self, count: u16) { + let attrs = self.attrs; + self.grid_mut().erase_cells(count, attrs); + } + + // CSI d + pub(crate) fn vpa(&mut self, row: u16) { + self.grid_mut().row_set(row - 1); + } + + // CSI h + #[allow(clippy::unused_self)] + pub(crate) fn sm(&mut self, params: &vte::Params) { + // nothing, i think? + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "unhandled SM mode: {}", + crate::perform::param_str(params) + ); + } + } + + // CSI ? h + pub(crate) fn decset(&mut self, params: &vte::Params) { + for param in params { + match param { + &[1] => self.set_mode(MODE_APPLICATION_CURSOR), + &[6] => self.grid_mut().set_origin_mode(true), + &[9] => self.set_mouse_mode(MouseProtocolMode::Press), + &[25] => self.clear_mode(MODE_HIDE_CURSOR), + &[47] => self.enter_alternate_grid(), + &[1000] => { + self.set_mouse_mode(MouseProtocolMode::PressRelease); + } + &[1002] => { + self.set_mouse_mode(MouseProtocolMode::ButtonMotion); + } + &[1003] => self.set_mouse_mode(MouseProtocolMode::AnyMotion), + &[1005] => { + self.set_mouse_encoding(MouseProtocolEncoding::Utf8); + } + &[1006] => { + self.set_mouse_encoding(MouseProtocolEncoding::Sgr); + } + &[1049] => { + self.decsc(); + self.alternate_grid.clear(); + self.enter_alternate_grid(); + } + &[2004] => self.set_mode(MODE_BRACKETED_PASTE), + ns => { + if log::log_enabled!(log::Level::Debug) { + let n = if ns.len() == 1 { + format!( + "{}", + // we just checked that ns.len() == 1, so 0 + // must be valid + ns[0] + ) + } else { + format!("{ns:?}") + }; + log::debug!("unhandled DECSET mode: {n}"); + } + } + } + } + } + + // CSI l + #[allow(clippy::unused_self)] + pub(crate) fn rm(&mut self, params: &vte::Params) { + // nothing, i think? + if log::log_enabled!(log::Level::Debug) { + log::debug!( + "unhandled RM mode: {}", + crate::perform::param_str(params) + ); + } + } + + // CSI ? l + pub(crate) fn decrst(&mut self, params: &vte::Params) { + for param in params { + match param { + &[1] => self.clear_mode(MODE_APPLICATION_CURSOR), + &[6] => self.grid_mut().set_origin_mode(false), + &[9] => self.clear_mouse_mode(MouseProtocolMode::Press), + &[25] => self.set_mode(MODE_HIDE_CURSOR), + &[47] => { + self.exit_alternate_grid(); + } + &[1000] => { + self.clear_mouse_mode(MouseProtocolMode::PressRelease); + } + &[1002] => { + self.clear_mouse_mode(MouseProtocolMode::ButtonMotion); + } + &[1003] => { + self.clear_mouse_mode(MouseProtocolMode::AnyMotion); + } + &[1005] => { + self.clear_mouse_encoding(MouseProtocolEncoding::Utf8); + } + &[1006] => { + self.clear_mouse_encoding(MouseProtocolEncoding::Sgr); + } + &[1049] => { + self.exit_alternate_grid(); + self.decrc(); + } + &[2004] => self.clear_mode(MODE_BRACKETED_PASTE), + ns => { + if log::log_enabled!(log::Level::Debug) { + let n = if ns.len() == 1 { + format!( + "{}", + // we just checked that ns.len() == 1, so 0 + // must be valid + ns[0] + ) + } else { + format!("{ns:?}") + }; + log::debug!("unhandled DECRST mode: {n}"); + } + } + } + } + } + + // CSI m + pub(crate) fn sgr(&mut self, params: &vte::Params) { + // XXX really i want to just be able to pass in a default Params + // instance with a 0 in it, but vte doesn't allow creating new Params + // instances + if params.is_empty() { + self.attrs = crate::attrs::Attrs::default(); + return; + } + + let mut iter = params.iter(); + + macro_rules! next_param { + () => { + match iter.next() { + Some(n) => n, + _ => return, + } + }; + } + + macro_rules! to_u8 { + ($n:expr) => { + if let Some(n) = u16_to_u8($n) { + n + } else { + return; + } + }; + } + + macro_rules! next_param_u8 { + () => { + if let &[n] = next_param!() { + to_u8!(n) + } else { + return; + } + }; + } + + loop { + match next_param!() { + &[0] => self.attrs = crate::attrs::Attrs::default(), + &[1] => self.attrs.set_bold(true), + &[3] => self.attrs.set_italic(true), + &[4] => self.attrs.set_underline(true), + &[7] => self.attrs.set_inverse(true), + &[22] => self.attrs.set_bold(false), + &[23] => self.attrs.set_italic(false), + &[24] => self.attrs.set_underline(false), + &[27] => self.attrs.set_inverse(false), + &[n] if (30..=37).contains(&n) => { + self.attrs.fgcolor = crate::Color::Idx(to_u8!(n) - 30); + } + &[38, 2, r, g, b] => { + self.attrs.fgcolor = + crate::Color::Rgb(to_u8!(r), to_u8!(g), to_u8!(b)); + } + &[38, 5, i] => { + self.attrs.fgcolor = crate::Color::Idx(to_u8!(i)); + } + &[38] => match next_param!() { + &[2] => { + let r = next_param_u8!(); + let g = next_param_u8!(); + let b = next_param_u8!(); + self.attrs.fgcolor = crate::Color::Rgb(r, g, b); + } + &[5] => { + self.attrs.fgcolor = + crate::Color::Idx(next_param_u8!()); + } + ns => { + if log::log_enabled!(log::Level::Debug) { + let n = if ns.len() == 1 { + format!( + "{}", + // we just checked that ns.len() == 1, so + // 0 must be valid + ns[0] + ) + } else { + format!("{ns:?}") + }; + log::debug!("unhandled SGR mode: 38 {n}"); + } + return; + } + }, + &[39] => { + self.attrs.fgcolor = crate::Color::Default; + } + &[n] if (40..=47).contains(&n) => { + self.attrs.bgcolor = crate::Color::Idx(to_u8!(n) - 40); + } + &[48, 2, r, g, b] => { + self.attrs.bgcolor = + crate::Color::Rgb(to_u8!(r), to_u8!(g), to_u8!(b)); + } + &[48, 5, i] => { + self.attrs.bgcolor = crate::Color::Idx(to_u8!(i)); + } + &[48] => match next_param!() { + &[2] => { + let r = next_param_u8!(); + let g = next_param_u8!(); + let b = next_param_u8!(); + self.attrs.bgcolor = crate::Color::Rgb(r, g, b); + } + &[5] => { + self.attrs.bgcolor = + crate::Color::Idx(next_param_u8!()); + } + ns => { + if log::log_enabled!(log::Level::Debug) { + let n = if ns.len() == 1 { + format!( + "{}", + // we just checked that ns.len() == 1, so + // 0 must be valid + ns[0] + ) + } else { + format!("{ns:?}") + }; + log::debug!("unhandled SGR mode: 48 {n}"); + } + return; + } + }, + &[49] => { + self.attrs.bgcolor = crate::Color::Default; + } + &[n] if (90..=97).contains(&n) => { + self.attrs.fgcolor = crate::Color::Idx(to_u8!(n) - 82); + } + &[n] if (100..=107).contains(&n) => { + self.attrs.bgcolor = crate::Color::Idx(to_u8!(n) - 92); + } + ns => { + if log::log_enabled!(log::Level::Debug) { + let n = if ns.len() == 1 { + format!( + "{}", + // we just checked that ns.len() == 1, so 0 + // must be valid + ns[0] + ) + } else { + format!("{ns:?}") + }; + log::debug!("unhandled SGR mode: {n}"); + } + } + } + } + } + + // CSI r + pub(crate) fn decstbm(&mut self, (top, bottom): (u16, u16)) { + self.grid_mut().set_scroll_region(top - 1, bottom - 1); + } + + // CSI t + #[allow(clippy::unused_self)] + pub(crate) fn xtwinops(&self, params: &vte::Params) { + let mut iter = params.iter(); + let op = iter.next().and_then(|x| x.first().copied()); + match op { + Some(8) => {} + _ => { + log::debug!( + "unhandled XTWINOPS: {}", + crate::perform::param_str(params) + ); + } + } + } + + // osc codes + + pub(crate) fn osc0(&mut self, s: &[u8]) { + self.osc1(s); + self.osc2(s); + } + + pub(crate) fn osc1(&mut self, s: &[u8]) { + if let Ok(s) = std::str::from_utf8(s) { + self.icon_name = s.to_string(); + } + } + + pub(crate) fn osc2(&mut self, s: &[u8]) { + if let Ok(s) = std::str::from_utf8(s) { + self.title = s.to_string(); + } + } +} + +fn u16_to_u8(i: u16) -> Option { + if i > u16::from(u8::MAX) { + None + } else { + // safe because we just ensured that the value fits in a u8 + Some(i.try_into().unwrap()) + } +} diff --git a/vt100/src/term.rs b/vt100/src/term.rs new file mode 100644 index 000000000..33a9d18c7 --- /dev/null +++ b/vt100/src/term.rs @@ -0,0 +1,592 @@ +// TODO: read all of this from terminfo + +pub trait BufWrite { + fn write_buf(&self, buf: &mut Vec); +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ClearScreen; + +impl BufWrite for ClearScreen { + fn write_buf(&self, buf: &mut Vec) { + buf.extend_from_slice(b"\x1b[H\x1b[J"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ClearRowForward; + +impl BufWrite for ClearRowForward { + fn write_buf(&self, buf: &mut Vec) { + buf.extend_from_slice(b"\x1b[K"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct Crlf; + +impl BufWrite for Crlf { + fn write_buf(&self, buf: &mut Vec) { + buf.extend_from_slice(b"\r\n"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct Backspace; + +impl BufWrite for Backspace { + fn write_buf(&self, buf: &mut Vec) { + buf.extend_from_slice(b"\x08"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct SaveCursor; + +impl BufWrite for SaveCursor { + fn write_buf(&self, buf: &mut Vec) { + buf.extend_from_slice(b"\x1b7"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct RestoreCursor; + +impl BufWrite for RestoreCursor { + fn write_buf(&self, buf: &mut Vec) { + buf.extend_from_slice(b"\x1b8"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct MoveTo { + row: u16, + col: u16, +} + +impl MoveTo { + pub fn new(pos: crate::grid::Pos) -> Self { + Self { + row: pos.row, + col: pos.col, + } + } +} + +impl BufWrite for MoveTo { + fn write_buf(&self, buf: &mut Vec) { + if self.row == 0 && self.col == 0 { + buf.extend_from_slice(b"\x1b[H"); + } else { + buf.extend_from_slice(b"\x1b["); + extend_itoa(buf, self.row + 1); + buf.push(b';'); + extend_itoa(buf, self.col + 1); + buf.push(b'H'); + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ClearAttrs; + +impl BufWrite for ClearAttrs { + fn write_buf(&self, buf: &mut Vec) { + buf.extend_from_slice(b"\x1b[m"); + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct Attrs { + fgcolor: Option, + bgcolor: Option, + bold: Option, + italic: Option, + underline: Option, + inverse: Option, +} + +impl Attrs { + pub fn fgcolor(mut self, fgcolor: crate::Color) -> Self { + self.fgcolor = Some(fgcolor); + self + } + + pub fn bgcolor(mut self, bgcolor: crate::Color) -> Self { + self.bgcolor = Some(bgcolor); + self + } + + pub fn bold(mut self, bold: bool) -> Self { + self.bold = Some(bold); + self + } + + pub fn italic(mut self, italic: bool) -> Self { + self.italic = Some(italic); + self + } + + pub fn underline(mut self, underline: bool) -> Self { + self.underline = Some(underline); + self + } + + pub fn inverse(mut self, inverse: bool) -> Self { + self.inverse = Some(inverse); + self + } +} + +impl BufWrite for Attrs { + #[allow(unused_assignments)] + #[allow(clippy::branches_sharing_code)] + fn write_buf(&self, buf: &mut Vec) { + if self.fgcolor.is_none() + && self.bgcolor.is_none() + && self.bold.is_none() + && self.italic.is_none() + && self.underline.is_none() + && self.inverse.is_none() + { + return; + } + + buf.extend_from_slice(b"\x1b["); + let mut first = true; + + macro_rules! write_param { + ($i:expr) => { + if first { + first = false; + } else { + buf.push(b';'); + } + extend_itoa(buf, $i); + }; + } + + if let Some(fgcolor) = self.fgcolor { + match fgcolor { + crate::Color::Default => { + write_param!(39); + } + crate::Color::Idx(i) => { + if i < 8 { + write_param!(i + 30); + } else if i < 16 { + write_param!(i + 82); + } else { + write_param!(38); + write_param!(5); + write_param!(i); + } + } + crate::Color::Rgb(r, g, b) => { + write_param!(38); + write_param!(2); + write_param!(r); + write_param!(g); + write_param!(b); + } + } + } + + if let Some(bgcolor) = self.bgcolor { + match bgcolor { + crate::Color::Default => { + write_param!(49); + } + crate::Color::Idx(i) => { + if i < 8 { + write_param!(i + 40); + } else if i < 16 { + write_param!(i + 92); + } else { + write_param!(48); + write_param!(5); + write_param!(i); + } + } + crate::Color::Rgb(r, g, b) => { + write_param!(48); + write_param!(2); + write_param!(r); + write_param!(g); + write_param!(b); + } + } + } + + if let Some(bold) = self.bold { + if bold { + write_param!(1); + } else { + write_param!(22); + } + } + + if let Some(italic) = self.italic { + if italic { + write_param!(3); + } else { + write_param!(23); + } + } + + if let Some(underline) = self.underline { + if underline { + write_param!(4); + } else { + write_param!(24); + } + } + + if let Some(inverse) = self.inverse { + if inverse { + write_param!(7); + } else { + write_param!(27); + } + } + + buf.push(b'm'); + } +} + +#[derive(Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct MoveRight { + count: u16, +} + +impl MoveRight { + pub fn new(count: u16) -> Self { + Self { count } + } +} + +impl Default for MoveRight { + fn default() -> Self { + Self { count: 1 } + } +} + +impl BufWrite for MoveRight { + fn write_buf(&self, buf: &mut Vec) { + match self.count { + 0 => {} + 1 => buf.extend_from_slice(b"\x1b[C"), + n => { + buf.extend_from_slice(b"\x1b["); + extend_itoa(buf, n); + buf.push(b'C'); + } + } + } +} + +#[derive(Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct EraseChar { + count: u16, +} + +impl EraseChar { + pub fn new(count: u16) -> Self { + Self { count } + } +} + +impl Default for EraseChar { + fn default() -> Self { + Self { count: 1 } + } +} + +impl BufWrite for EraseChar { + fn write_buf(&self, buf: &mut Vec) { + match self.count { + 0 => {} + 1 => buf.extend_from_slice(b"\x1b[X"), + n => { + buf.extend_from_slice(b"\x1b["); + extend_itoa(buf, n); + buf.push(b'X'); + } + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct HideCursor { + state: bool, +} + +impl HideCursor { + pub fn new(state: bool) -> Self { + Self { state } + } +} + +impl BufWrite for HideCursor { + fn write_buf(&self, buf: &mut Vec) { + if self.state { + buf.extend_from_slice(b"\x1b[?25l"); + } else { + buf.extend_from_slice(b"\x1b[?25h"); + } + } +} + +#[derive(Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct MoveFromTo { + from: crate::grid::Pos, + to: crate::grid::Pos, +} + +impl MoveFromTo { + pub fn new(from: crate::grid::Pos, to: crate::grid::Pos) -> Self { + Self { from, to } + } +} + +impl BufWrite for MoveFromTo { + fn write_buf(&self, buf: &mut Vec) { + if self.to.row == self.from.row + 1 && self.to.col == 0 { + crate::term::Crlf.write_buf(buf); + } else if self.from.row == self.to.row && self.from.col < self.to.col + { + crate::term::MoveRight::new(self.to.col - self.from.col) + .write_buf(buf); + } else if self.to != self.from { + crate::term::MoveTo::new(self.to).write_buf(buf); + } + } +} + +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ChangeTitle<'a> { + icon_name: &'a str, + title: &'a str, + prev_icon_name: &'a str, + prev_title: &'a str, +} + +impl<'a> ChangeTitle<'a> { + pub fn new( + icon_name: &'a str, + title: &'a str, + prev_icon_name: &'a str, + prev_title: &'a str, + ) -> Self { + Self { + icon_name, + title, + prev_icon_name, + prev_title, + } + } +} + +impl<'a> BufWrite for ChangeTitle<'a> { + fn write_buf(&self, buf: &mut Vec) { + if self.icon_name == self.title + && (self.icon_name != self.prev_icon_name + || self.title != self.prev_title) + { + buf.extend_from_slice(b"\x1b]0;"); + buf.extend_from_slice(self.icon_name.as_bytes()); + buf.push(b'\x07'); + } else { + if self.icon_name != self.prev_icon_name { + buf.extend_from_slice(b"\x1b]1;"); + buf.extend_from_slice(self.icon_name.as_bytes()); + buf.push(b'\x07'); + } + if self.title != self.prev_title { + buf.extend_from_slice(b"\x1b]2;"); + buf.extend_from_slice(self.title.as_bytes()); + buf.push(b'\x07'); + } + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ApplicationKeypad { + state: bool, +} + +impl ApplicationKeypad { + pub fn new(state: bool) -> Self { + Self { state } + } +} + +impl BufWrite for ApplicationKeypad { + fn write_buf(&self, buf: &mut Vec) { + if self.state { + buf.extend_from_slice(b"\x1b="); + } else { + buf.extend_from_slice(b"\x1b>"); + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct ApplicationCursor { + state: bool, +} + +impl ApplicationCursor { + pub fn new(state: bool) -> Self { + Self { state } + } +} + +impl BufWrite for ApplicationCursor { + fn write_buf(&self, buf: &mut Vec) { + if self.state { + buf.extend_from_slice(b"\x1b[?1h"); + } else { + buf.extend_from_slice(b"\x1b[?1l"); + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct BracketedPaste { + state: bool, +} + +impl BracketedPaste { + pub fn new(state: bool) -> Self { + Self { state } + } +} + +impl BufWrite for BracketedPaste { + fn write_buf(&self, buf: &mut Vec) { + if self.state { + buf.extend_from_slice(b"\x1b[?2004h"); + } else { + buf.extend_from_slice(b"\x1b[?2004l"); + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct MouseProtocolMode { + mode: crate::MouseProtocolMode, + prev: crate::MouseProtocolMode, +} + +impl MouseProtocolMode { + pub fn new( + mode: crate::MouseProtocolMode, + prev: crate::MouseProtocolMode, + ) -> Self { + Self { mode, prev } + } +} + +impl BufWrite for MouseProtocolMode { + fn write_buf(&self, buf: &mut Vec) { + if self.mode == self.prev { + return; + } + + match self.mode { + crate::MouseProtocolMode::None => match self.prev { + crate::MouseProtocolMode::None => {} + crate::MouseProtocolMode::Press => { + buf.extend_from_slice(b"\x1b[?9l"); + } + crate::MouseProtocolMode::PressRelease => { + buf.extend_from_slice(b"\x1b[?1000l"); + } + crate::MouseProtocolMode::ButtonMotion => { + buf.extend_from_slice(b"\x1b[?1002l"); + } + crate::MouseProtocolMode::AnyMotion => { + buf.extend_from_slice(b"\x1b[?1003l"); + } + }, + crate::MouseProtocolMode::Press => { + buf.extend_from_slice(b"\x1b[?9h"); + } + crate::MouseProtocolMode::PressRelease => { + buf.extend_from_slice(b"\x1b[?1000h"); + } + crate::MouseProtocolMode::ButtonMotion => { + buf.extend_from_slice(b"\x1b[?1002h"); + } + crate::MouseProtocolMode::AnyMotion => { + buf.extend_from_slice(b"\x1b[?1003h"); + } + } + } +} + +#[derive(Default, Debug)] +#[must_use = "this struct does nothing unless you call write_buf"] +pub struct MouseProtocolEncoding { + encoding: crate::MouseProtocolEncoding, + prev: crate::MouseProtocolEncoding, +} + +impl MouseProtocolEncoding { + pub fn new( + encoding: crate::MouseProtocolEncoding, + prev: crate::MouseProtocolEncoding, + ) -> Self { + Self { encoding, prev } + } +} + +impl BufWrite for MouseProtocolEncoding { + fn write_buf(&self, buf: &mut Vec) { + if self.encoding == self.prev { + return; + } + + match self.encoding { + crate::MouseProtocolEncoding::Default => match self.prev { + crate::MouseProtocolEncoding::Default => {} + crate::MouseProtocolEncoding::Utf8 => { + buf.extend_from_slice(b"\x1b[?1005l"); + } + crate::MouseProtocolEncoding::Sgr => { + buf.extend_from_slice(b"\x1b[?1006l"); + } + }, + crate::MouseProtocolEncoding::Utf8 => { + buf.extend_from_slice(b"\x1b[?1005h"); + } + crate::MouseProtocolEncoding::Sgr => { + buf.extend_from_slice(b"\x1b[?1006h"); + } + } + } +} + +fn extend_itoa(buf: &mut Vec, i: I) { + let mut itoa_buf = itoa::Buffer::new(); + buf.extend_from_slice(itoa_buf.format(i).as_bytes()); +}