use std::collections::VecDeque;
use super::cell::Cell;
#[derive(Copy, Clone, Debug, Default, PartialEq, Hash)]
pub enum Charset {
#[default]
Ascii,
LineDrawing,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Hash)]
pub enum ActiveCharset {
#[default]
G0,
G1,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Hash)]
pub enum CursorShape {
#[default]
Default,
BlinkBlock,
SteadyBlock,
BlinkUnderline,
SteadyUnderline,
BlinkBar,
SteadyBar,
}
impl CursorShape {
pub fn from_param(n: u8) -> Self {
match n {
1 => Self::BlinkBlock,
2 => Self::SteadyBlock,
3 => Self::BlinkUnderline,
4 => Self::SteadyUnderline,
5 => Self::BlinkBar,
6 => Self::SteadyBar,
_ => Self::Default,
}
}
pub fn to_param(self) -> u8 {
match self {
Self::Default => 0,
Self::BlinkBlock => 1,
Self::SteadyBlock => 2,
Self::BlinkUnderline => 3,
Self::SteadyUnderline => 4,
Self::BlinkBar => 5,
Self::SteadyBar => 6,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TerminalModes {
pub cursor_key_mode: bool, pub bracketed_paste: bool, pub autowrap_mode: bool, pub focus_reporting: bool, pub mouse_modes: MouseModes,
pub mouse_encoding: MouseEncoding,
pub keypad_app_mode: bool, pub cursor_shape: CursorShape,
pub g0_charset: Charset,
pub g1_charset: Charset,
pub active_charset: ActiveCharset,
}
impl Default for TerminalModes {
fn default() -> Self {
Self {
cursor_key_mode: false,
bracketed_paste: false,
autowrap_mode: true,
focus_reporting: false,
mouse_modes: MouseModes::default(),
mouse_encoding: MouseEncoding::X10,
keypad_app_mode: false,
cursor_shape: CursorShape::Default,
g0_charset: Charset::Ascii,
g1_charset: Charset::Ascii,
active_charset: ActiveCharset::G0,
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Hash)]
pub enum MouseMode {
#[default]
Off,
Click, Button, Any, }
impl MouseMode {
pub fn from_param(p: u16) -> Option<Self> {
match p {
1000 => Some(Self::Click),
1002 => Some(Self::Button),
1003 => Some(Self::Any),
_ => None,
}
}
pub fn to_param(self) -> u16 {
match self {
Self::Off => 0,
Self::Click => 1000,
Self::Button => 1002,
Self::Any => 1003,
}
}
pub fn is_enabled(self) -> bool {
!matches!(self, Self::Off)
}
}
#[derive(Clone, Debug, Default, PartialEq, Hash)]
pub struct MouseModes {
pub click: bool, pub button: bool, pub any: bool, }
impl MouseModes {
pub fn set(&mut self, param: u16, enable: bool) {
match param {
1000 => self.click = enable,
1002 => self.button = enable,
1003 => self.any = enable,
_ => {}
}
}
pub fn effective(&self) -> MouseMode {
if self.any { MouseMode::Any }
else if self.button { MouseMode::Button }
else if self.click { MouseMode::Click }
else { MouseMode::Off }
}
pub fn is_enabled(&self) -> bool {
self.click || self.button || self.any
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Hash)]
pub enum MouseEncoding {
#[default]
X10,
Utf8, Sgr, }
impl MouseEncoding {
pub fn from_param(p: u16) -> Option<Self> {
match p {
1005 => Some(Self::Utf8),
1006 => Some(Self::Sgr),
_ => None,
}
}
}
pub struct Grid {
pub cols: u16,
pub rows: u16,
pub cells: VecDeque<Vec<Cell>>,
pub cursor_x: u16,
pub cursor_y: u16,
pub wrap_pending: bool,
pub scroll_top: u16,
pub scroll_bottom: u16,
pub cursor_visible: bool,
pub modes: TerminalModes,
pub tab_stops: Vec<bool>,
pub scrollback_len: usize,
pub scrollback_limit: usize,
pub pending_start: usize,
}
pub struct SavedGrid {
pub visible_cells: VecDeque<Vec<Cell>>,
pub scrollback_limit: usize,
}
pub fn default_tab_stops(cols: u16) -> Vec<bool> {
(0..cols).map(|c| c > 0 && c % 8 == 0).collect()
}
impl Grid {
pub fn new(cols: u16, rows: u16, scrollback_limit: usize) -> Self {
let (cols, rows) = sanitize_dimensions(cols, rows);
Self {
cols,
rows,
cells: (0..rows as usize).map(|_| vec![Cell::default(); cols as usize]).collect(),
cursor_x: 0,
cursor_y: 0,
wrap_pending: false,
scroll_top: 0,
scroll_bottom: rows - 1,
cursor_visible: true,
modes: TerminalModes::default(),
tab_stops: default_tab_stops(cols),
scrollback_len: 0,
scrollback_limit,
pending_start: 0,
}
}
pub fn visible_row(&self, y: usize) -> &Vec<Cell> {
&self.cells[self.scrollback_len + y]
}
pub fn visible_row_mut(&mut self, y: usize) -> &mut Vec<Cell> {
let offset = self.scrollback_len;
&mut self.cells[offset + y]
}
pub fn visible_rows(&self) -> impl Iterator<Item = &Vec<Cell>> {
self.cells.iter().skip(self.scrollback_len).take(self.rows as usize)
}
pub fn visible_rows_mut(&mut self) -> impl Iterator<Item = &mut Vec<Cell>> {
let skip = self.scrollback_len;
let take = self.rows as usize;
self.cells.iter_mut().skip(skip).take(take)
}
pub fn visible_row_count(&self) -> usize {
self.cells.len() - self.scrollback_len
}
pub fn remove_visible_row(&mut self, y: usize) -> Vec<Cell> {
self.cells.remove(self.scrollback_len + y).unwrap()
}
pub fn insert_visible_row(&mut self, y: usize, row: Vec<Cell>) {
self.cells.insert(self.scrollback_len + y, row);
}
pub fn next_tab_stop(&self, col: u16) -> u16 {
for c in (col as usize + 1)..self.tab_stops.len() {
if self.tab_stops[c] {
return c as u16;
}
}
self.cols - 1
}
pub fn scroll_up(&mut self, in_alt_screen: bool, fill: Cell) {
let top = self.scroll_top as usize;
let bottom = self.scroll_bottom as usize;
let visible_len = self.cells.len() - self.scrollback_len;
if !in_alt_screen && top == 0 && self.scrollback_limit > 0 {
self.scrollback_len += 1;
if self.scrollback_len > self.scrollback_limit {
self.cells.pop_front();
self.scrollback_len -= 1;
if self.pending_start > 0 { self.pending_start -= 1; }
}
self.cells.push_back(vec![fill; self.cols as usize]);
} else if top <= bottom && bottom < visible_len {
if top == 0 && bottom == visible_len - 1 {
self.cells.remove(self.scrollback_len);
self.cells.push_back(vec![fill; self.cols as usize]);
} else {
self.cells.remove(self.scrollback_len + top);
self.cells.insert(self.scrollback_len + bottom, vec![fill; self.cols as usize]);
}
}
}
pub fn scroll_down(&mut self, fill: Cell) {
let top = self.scroll_top as usize;
let bottom = self.scroll_bottom as usize;
let visible_len = self.cells.len() - self.scrollback_len;
if top <= bottom && bottom < visible_len {
self.cells.remove(self.scrollback_len + bottom);
self.cells.insert(self.scrollback_len + top, vec![fill; self.cols as usize]);
}
}
pub fn resize(&mut self, cols: u16, rows: u16) {
let (cols, rows) = sanitize_dimensions(cols, rows);
self.cols = cols;
self.rows = rows;
let rows_usize = rows as usize;
let visible_len = self.cells.len() - self.scrollback_len;
if visible_len > rows_usize {
let excess = visible_len - rows_usize;
for _ in 0..excess { self.cells.pop_back(); }
} else if visible_len < rows_usize {
let deficit = rows_usize - visible_len;
for _ in 0..deficit {
self.cells.push_back(vec![Cell::default(); cols as usize]);
}
}
let cols_usize = cols as usize;
for row in self.cells.iter_mut().skip(self.scrollback_len) {
if row.len() > cols_usize && cols_usize > 0 {
let last = cols_usize - 1;
if row[last].width == 2 {
row[last] = Cell::default();
}
}
row.resize(cols_usize, Cell::default());
}
if self.cursor_x >= cols { self.cursor_x = cols - 1; }
if self.cursor_y >= rows { self.cursor_y = rows - 1; }
self.wrap_pending = false;
self.scroll_top = 0;
self.scroll_bottom = rows - 1;
self.tab_stops = default_tab_stops(cols);
}
}
pub fn sanitize_dimensions(cols: u16, rows: u16) -> (u16, u16) {
(cols.max(1), rows.max(1))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_zero_dimensions() {
assert_eq!(sanitize_dimensions(0, 0), (1, 1));
assert_eq!(sanitize_dimensions(80, 0), (80, 1));
assert_eq!(sanitize_dimensions(0, 24), (1, 24));
}
#[test]
fn grid_new_creates_correct_size() {
let grid = Grid::new(80, 24, 0);
assert_eq!(grid.visible_row_count(), 24);
assert_eq!(grid.visible_row(0).len(), 80);
}
#[test]
fn grid_new_zero_dimensions() {
let grid = Grid::new(0, 0, 0);
assert_eq!(grid.cols, 1);
assert_eq!(grid.rows, 1);
assert_eq!(grid.visible_row_count(), 1);
assert_eq!(grid.visible_row(0).len(), 1);
}
#[test]
fn grid_resize() {
let mut grid = Grid::new(80, 24, 0);
grid.cursor_x = 79;
grid.cursor_y = 23;
grid.resize(40, 12);
assert_eq!(grid.visible_row_count(), 12);
assert_eq!(grid.visible_row(0).len(), 40);
assert_eq!(grid.cursor_x, 39);
assert_eq!(grid.cursor_y, 11);
}
#[test]
fn grid_resize_zero() {
let mut grid = Grid::new(80, 24, 0);
grid.resize(0, 0);
assert_eq!(grid.cols, 1);
assert_eq!(grid.rows, 1);
}
#[test]
fn grid_scroll_up() {
let mut grid = Grid::new(10, 3, 100);
grid.visible_row_mut(0)[0].c = 'A';
grid.scroll_up(false, Cell::default());
assert_eq!(grid.scrollback_len, 1);
assert_eq!(grid.scrollback_len - grid.pending_start, 1);
assert_eq!(grid.visible_row_count(), 3);
assert_eq!(grid.cells[0][0].c, 'A');
assert_eq!(grid.visible_row(0)[0].c, ' ');
}
#[test]
fn grid_scroll_up_alt_screen_no_scrollback() {
let mut grid = Grid::new(10, 3, 100);
grid.visible_row_mut(0)[0].c = 'A';
grid.scroll_up(true, Cell::default());
assert_eq!(grid.scrollback_len, 0);
}
#[test]
fn grid_scroll_up_respects_limit() {
let mut grid = Grid::new(10, 3, 3);
for _ in 0..5 {
grid.scroll_up(false, Cell::default());
}
assert_eq!(grid.scrollback_len, 3);
}
#[test]
fn pending_scrollback_respects_limit() {
let mut grid = Grid::new(10, 3, 5);
for _ in 0..20 {
grid.scroll_up(false, Cell::default());
}
let pending_count = grid.scrollback_len - grid.pending_start;
assert_eq!(pending_count, 5, "pending scrollback should be exactly at limit, got {}", pending_count);
}
#[test]
fn terminal_modes_default() {
let modes = TerminalModes::default();
assert!(modes.autowrap_mode);
assert!(!modes.cursor_key_mode);
assert!(!modes.bracketed_paste);
assert_eq!(modes.mouse_modes, MouseModes::default());
assert_eq!(modes.cursor_shape, CursorShape::Default);
}
fn paint_checkerboard(grid: &mut Grid) {
for r in 0..grid.rows as usize {
for c in 0..grid.cols as usize {
grid.visible_row_mut(r)[c].c = if (r + c) % 2 == 0 { 'A' } else { 'B' };
}
}
}
fn assert_checkerboard(grid: &Grid, rows: usize, cols: usize) {
for r in 0..rows {
for c in 0..cols {
let expected = if (r + c) % 2 == 0 { 'A' } else { 'B' };
assert_eq!(grid.visible_row(r)[c].c, expected,
"checkerboard mismatch at ({}, {}): expected '{}', got '{}'",
r, c, expected, grid.visible_row(r)[c].c);
}
}
}
#[test]
fn resize_horizontal_expand_preserves_content() {
let mut grid = Grid::new(5, 4, 0);
paint_checkerboard(&mut grid);
grid.resize(10, 4); assert_eq!(grid.cols, 10);
assert_eq!(grid.visible_row(0).len(), 10);
assert_checkerboard(&grid, 4, 5);
for r in 0..4 {
for c in 5..10 {
assert_eq!(grid.visible_row(r)[c].c, ' ',
"new cell at ({}, {}) should be blank", r, c);
}
}
}
#[test]
fn resize_horizontal_shrink_preserves_visible_content() {
let mut grid = Grid::new(10, 4, 0);
paint_checkerboard(&mut grid);
grid.resize(5, 4); assert_eq!(grid.cols, 5);
assert_eq!(grid.visible_row(0).len(), 5);
assert_checkerboard(&grid, 4, 5);
}
#[test]
fn resize_horizontal_shrink_then_expand_loses_truncated() {
let mut grid = Grid::new(10, 3, 0);
paint_checkerboard(&mut grid);
grid.resize(5, 3); grid.resize(10, 3); assert_checkerboard(&grid, 3, 5);
for r in 0..3 {
for c in 5..10 {
assert_eq!(grid.visible_row(r)[c].c, ' ',
"truncated cell at ({}, {}) should be blank after re-expand", r, c);
}
}
}
#[test]
fn resize_vertical_expand_preserves_content() {
let mut grid = Grid::new(6, 3, 0);
paint_checkerboard(&mut grid);
grid.resize(6, 8); assert_eq!(grid.rows, 8);
assert_eq!(grid.visible_row_count(), 8);
assert_checkerboard(&grid, 3, 6);
for r in 3..8 {
for c in 0..6 {
assert_eq!(grid.visible_row(r)[c].c, ' ',
"new cell at ({}, {}) should be blank", r, c);
}
}
}
#[test]
fn resize_vertical_shrink_preserves_visible_content() {
let mut grid = Grid::new(6, 8, 0);
paint_checkerboard(&mut grid);
grid.resize(6, 3); assert_eq!(grid.rows, 3);
assert_eq!(grid.visible_row_count(), 3);
assert_checkerboard(&grid, 3, 6);
}
#[test]
fn resize_vertical_shrink_then_expand_loses_truncated() {
let mut grid = Grid::new(6, 8, 0);
paint_checkerboard(&mut grid);
grid.resize(6, 3); grid.resize(6, 8); assert_checkerboard(&grid, 3, 6);
for r in 3..8 {
for c in 0..6 {
assert_eq!(grid.visible_row(r)[c].c, ' ',
"truncated cell at ({}, {}) should be blank after re-expand", r, c);
}
}
}
#[test]
fn resize_both_expand() {
let mut grid = Grid::new(4, 3, 0);
paint_checkerboard(&mut grid);
grid.resize(8, 6); assert_checkerboard(&grid, 3, 4);
for r in 0..3 {
for c in 4..8 {
assert_eq!(grid.visible_row(r)[c].c, ' ',
"new col cell at ({}, {}) should be blank", r, c);
}
}
for r in 3..6 {
for c in 0..8 {
assert_eq!(grid.visible_row(r)[c].c, ' ',
"new row cell at ({}, {}) should be blank", r, c);
}
}
}
#[test]
fn resize_both_shrink() {
let mut grid = Grid::new(10, 8, 0);
paint_checkerboard(&mut grid);
grid.resize(5, 4); assert_eq!(grid.visible_row_count(), 4);
assert_eq!(grid.visible_row(0).len(), 5);
assert_checkerboard(&grid, 4, 5);
}
#[test]
fn resize_expand_cols_shrink_rows() {
let mut grid = Grid::new(4, 8, 0);
paint_checkerboard(&mut grid);
grid.resize(10, 3); assert_eq!(grid.visible_row_count(), 3);
assert_eq!(grid.visible_row(0).len(), 10);
assert_checkerboard(&grid, 3, 4);
for r in 0..3 {
for c in 4..10 {
assert_eq!(grid.visible_row(r)[c].c, ' ',
"new cell at ({}, {}) should be blank", r, c);
}
}
}
#[test]
fn resize_shrink_cols_expand_rows() {
let mut grid = Grid::new(10, 3, 0);
paint_checkerboard(&mut grid);
grid.resize(4, 8); assert_eq!(grid.visible_row_count(), 8);
assert_eq!(grid.visible_row(0).len(), 4);
assert_checkerboard(&grid, 3, 4);
for r in 3..8 {
for c in 0..4 {
assert_eq!(grid.visible_row(r)[c].c, ' ',
"new row cell at ({}, {}) should be blank", r, c);
}
}
}
#[test]
fn resize_multiple_sequential_preserves_overlap() {
let mut grid = Grid::new(10, 10, 0);
paint_checkerboard(&mut grid);
grid.resize(5, 5);
assert_checkerboard(&grid, 5, 5);
grid.resize(8, 12);
assert_checkerboard(&grid, 5, 5);
grid.resize(3, 3);
assert_checkerboard(&grid, 3, 3);
grid.resize(20, 20);
assert_checkerboard(&grid, 3, 3);
}
#[test]
fn resize_horizontal_shrink_clamps_cursor() {
let mut grid = Grid::new(10, 5, 0);
grid.cursor_x = 8;
grid.cursor_y = 2;
grid.resize(5, 5);
assert_eq!(grid.cursor_x, 4, "cursor_x should clamp to cols-1");
assert_eq!(grid.cursor_y, 2, "cursor_y should not change");
}
#[test]
fn resize_vertical_shrink_clamps_cursor() {
let mut grid = Grid::new(10, 10, 0);
grid.cursor_x = 3;
grid.cursor_y = 8;
grid.resize(10, 5);
assert_eq!(grid.cursor_x, 3, "cursor_x should not change");
assert_eq!(grid.cursor_y, 4, "cursor_y should clamp to rows-1");
}
#[test]
fn resize_both_shrink_clamps_cursor() {
let mut grid = Grid::new(20, 20, 0);
grid.cursor_x = 15;
grid.cursor_y = 18;
grid.resize(5, 5);
assert_eq!(grid.cursor_x, 4);
assert_eq!(grid.cursor_y, 4);
}
#[test]
fn resize_expand_preserves_cursor() {
let mut grid = Grid::new(10, 10, 0);
grid.cursor_x = 5;
grid.cursor_y = 7;
grid.resize(20, 20);
assert_eq!(grid.cursor_x, 5, "cursor_x should not change on expand");
assert_eq!(grid.cursor_y, 7, "cursor_y should not change on expand");
}
#[test]
fn resize_same_dimensions_preserves_everything() {
let mut grid = Grid::new(8, 6, 0);
paint_checkerboard(&mut grid);
grid.cursor_x = 3;
grid.cursor_y = 2;
grid.resize(8, 6); assert_checkerboard(&grid, 6, 8);
assert_eq!(grid.cursor_x, 3);
assert_eq!(grid.cursor_y, 2);
}
#[test]
fn resize_resets_scroll_region() {
let mut grid = Grid::new(80, 24, 0);
grid.scroll_top = 5;
grid.scroll_bottom = 18;
grid.resize(80, 30);
assert_eq!(grid.scroll_top, 0);
assert_eq!(grid.scroll_bottom, 29, "scroll_bottom should be rows-1");
}
#[test]
fn resize_resets_tab_stops() {
let mut grid = Grid::new(80, 24, 0);
grid.tab_stops[3] = true;
grid.resize(40, 24);
assert_eq!(grid.tab_stops.len(), 40);
assert!(!grid.tab_stops[0]);
assert!(grid.tab_stops[8]);
assert!(grid.tab_stops[16]);
assert!(!grid.tab_stops[3], "custom tab stop should be gone after resize");
}
}