From ad507849196dbdb864044444eee3b732f14a5c87 Mon Sep 17 00:00:00 2001 From: Tom You Date: Tue, 7 Jul 2026 00:03:40 -0500 Subject: [PATCH] feat(core): add terminal grid model --- rabby-core/src/lib.rs | 1 + rabby-core/src/terminal_model/mod.rs | 109 +++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 rabby-core/src/terminal_model/mod.rs diff --git a/rabby-core/src/lib.rs b/rabby-core/src/lib.rs index 830e48a..177f096 100644 --- a/rabby-core/src/lib.rs +++ b/rabby-core/src/lib.rs @@ -5,6 +5,7 @@ pub mod config; pub mod feature; +pub mod terminal_model; pub mod workspace; pub use config::{AppConfig, ShortcutBinding}; diff --git a/rabby-core/src/terminal_model/mod.rs b/rabby-core/src/terminal_model/mod.rs new file mode 100644 index 0000000..ed81456 --- /dev/null +++ b/rabby-core/src/terminal_model/mod.rs @@ -0,0 +1,109 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Cell { + pub ch: char, +} + +impl Default for Cell { + fn default() -> Self { + Self { ch: ' ' } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TerminalGrid { + cols: u16, + rows: u16, + cursor_col: u16, + cursor_row: u16, + cells: Vec, +} + +impl TerminalGrid { + pub fn new(cols: u16, rows: u16) -> Self { + let size = usize::from(cols) * usize::from(rows); + Self { + cols, + rows, + cursor_col: 0, + cursor_row: 0, + cells: vec![Cell::default(); size], + } + } + + pub fn write_utf8(&mut self, bytes: &[u8]) { + let text = String::from_utf8_lossy(bytes); + for ch in text.chars() { + match ch { + '\r' => self.cursor_col = 0, + '\n' => self.newline(), + _ => self.put_char(ch), + } + } + } + + pub fn visible_text(&self) -> String { + self.line_text(0) + } + + pub fn line_text(&self, row: u16) -> String { + if row >= self.rows { + return String::new(); + } + let start = usize::from(row) * usize::from(self.cols); + let end = start + usize::from(self.cols); + self.cells[start..end].iter().map(|cell| cell.ch).collect() + } + + fn put_char(&mut self, ch: char) { + if self.cursor_row >= self.rows || self.cursor_col >= self.cols { + return; + } + let index = + usize::from(self.cursor_row) * usize::from(self.cols) + usize::from(self.cursor_col); + self.cells[index].ch = ch; + self.cursor_col += 1; + if self.cursor_col >= self.cols { + self.newline(); + } + } + + fn newline(&mut self) { + self.cursor_col = 0; + if self.cursor_row + 1 < self.rows { + self.cursor_row += 1; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn writes_ascii_text_to_grid() { + let mut grid = TerminalGrid::new(8, 2); + + grid.write_utf8(b"hello"); + + assert_eq!(grid.visible_text(), "hello "); + } + + #[test] + fn handles_newline_and_carriage_return() { + let mut grid = TerminalGrid::new(6, 2); + + grid.write_utf8(b"abc\rZ\ndef"); + + assert_eq!(grid.line_text(0), "Zbc "); + assert_eq!(grid.line_text(1), "def "); + } + + #[test] + fn tolerates_utf8_double_width_input() { + let mut grid = TerminalGrid::new(6, 1); + + grid.write_utf8("한A".as_bytes()); + + assert!(grid.line_text(0).starts_with("한A")); + } +}