Skip to main content

ratatui_toolkit/vt100_term/
grid.rs

1//! Terminal grid with VecDeque-based scrollback
2//!
3//! Inspired by mprocs' implementation using VecDeque for efficient
4//! circular buffer scrollback.
5
6use super::cell::Cell;
7use std::collections::VecDeque;
8
9/// Terminal size
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct Size {
12    pub rows: usize,
13    pub cols: usize,
14}
15
16/// A single row in the grid
17#[derive(Debug, Clone)]
18pub struct Row {
19    cells: Vec<Cell>,
20    wrapped: bool, // Whether this line is wrapped from previous
21}
22
23impl Row {
24    /// Create a new row with given width
25    fn new(cols: usize) -> Self {
26        Self {
27            cells: vec![Cell::default(); cols],
28            wrapped: false,
29        }
30    }
31
32    /// Get number of columns
33    pub fn cols(&self) -> usize {
34        self.cells.len()
35    }
36
37    /// Check if row is wrapped
38    pub fn wrapped(&self) -> bool {
39        self.wrapped
40    }
41
42    /// Set wrapped status
43    pub fn set_wrapped(&mut self, wrapped: bool) {
44        self.wrapped = wrapped;
45    }
46
47    /// Get cell at column
48    pub fn get(&self, col: usize) -> Option<&Cell> {
49        self.cells.get(col)
50    }
51
52    /// Get mutable cell at column
53    pub fn get_mut(&mut self, col: usize) -> Option<&mut Cell> {
54        self.cells.get_mut(col)
55    }
56
57    /// Set cell at column
58    pub fn set(&mut self, col: usize, cell: Cell) {
59        if col < self.cells.len() {
60            self.cells[col] = cell;
61        }
62    }
63
64    /// Extract text content from this row
65    pub fn text_content(&self, start: usize, width: usize) -> String {
66        let end = (start + width).min(self.cells.len());
67        self.cells[start..end]
68            .iter()
69            .map(|cell| cell.text.as_str())
70            .collect::<String>()
71            .trim_end()
72            .to_string()
73    }
74}
75
76/// Grid with VecDeque-based scrollback
77///
78/// Layout: [scrollback_rows... | visible_rows...]
79/// row0() calculates where visible area starts
80#[derive(Debug, Clone)]
81pub struct Grid {
82    /// Current visible size
83    size: Size,
84
85    /// All rows (scrollback + visible)
86    rows: VecDeque<Row>,
87
88    /// Maximum scrollback lines
89    scrollback_len: usize,
90
91    /// Current scrollback offset (0 = at bottom)
92    scrollback_offset: usize,
93}
94
95impl Grid {
96    /// Create a new grid
97    pub fn new(rows: usize, cols: usize, scrollback_len: usize) -> Self {
98        let size = Size { rows, cols };
99        let mut grid_rows = VecDeque::with_capacity(rows + scrollback_len);
100
101        for _ in 0..rows {
102            grid_rows.push_back(Row::new(cols));
103        }
104
105        Self {
106            size,
107            rows: grid_rows,
108            scrollback_len,
109            scrollback_offset: 0,
110        }
111    }
112
113    /// Get grid size
114    pub fn size(&self) -> Size {
115        self.size
116    }
117
118    /// Get current scrollback offset
119    pub fn scrollback(&self) -> usize {
120        self.scrollback_offset
121    }
122
123    /// Get maximum scrollback length
124    pub fn scrollback_len(&self) -> usize {
125        self.scrollback_len
126    }
127
128    /// Set scrollback offset
129    pub fn set_scrollback(&mut self, offset: usize) {
130        self.scrollback_offset = offset.min(self.row0());
131    }
132
133    /// Scroll screen up (view older content)
134    pub fn scroll_screen_up(&mut self, n: usize) {
135        self.scrollback_offset = (self.scrollback_offset + n).min(self.row0());
136    }
137
138    /// Scroll screen down (view newer content)
139    pub fn scroll_screen_down(&mut self, n: usize) {
140        self.scrollback_offset = self.scrollback_offset.saturating_sub(n);
141    }
142
143    /// Calculate where visible area starts in the deque
144    fn row0(&self) -> usize {
145        self.rows.len().saturating_sub(self.size.rows)
146    }
147
148    /// Get visible rows iterator
149    pub fn visible_rows(&self) -> impl Iterator<Item = &Row> {
150        let start = self.row0().saturating_sub(self.scrollback_offset);
151        self.rows.iter().skip(start).take(self.size.rows)
152    }
153
154    /// Get cell at position (relative to visible area)
155    pub fn cell(&self, row: usize, col: usize) -> Option<&Cell> {
156        let start = self.row0().saturating_sub(self.scrollback_offset);
157        let actual_row = start + row;
158        self.rows.get(actual_row)?.get(col)
159    }
160
161    /// Get mutable cell at position (relative to current visible bottom)
162    pub fn cell_mut(&mut self, row: usize, col: usize) -> Option<&mut Cell> {
163        let row_index = self.row0() + row;
164        self.rows.get_mut(row_index)?.get_mut(col)
165    }
166
167    /// Get row (relative to visible area)
168    pub fn row(&self, row: usize) -> Option<&Row> {
169        let start = self.row0();
170        self.rows.get(start + row)
171    }
172
173    /// Scroll content up (add new line at bottom, old line goes to scrollback)
174    pub fn scroll_up(&mut self, count: usize) {
175        for _ in 0..count.min(self.size.rows) {
176            let row0 = self.row0();
177
178            // Add new empty row at bottom
179            self.rows
180                .insert(row0 + self.size.rows, Row::new(self.size.cols));
181
182            // Remove top row and add to scrollback if enabled
183            if self.scrollback_len > 0 {
184                if let Some(removed) = self.rows.remove(row0) {
185                    // Add to scrollback (front of deque)
186                    self.rows.insert(row0, removed);
187                }
188
189                // Limit scrollback size
190                while self.rows.len() > self.size.rows + self.scrollback_len {
191                    self.rows.pop_front();
192                }
193
194                // Adjust scroll offset if user was scrolled up
195                if self.scrollback_offset > 0 {
196                    self.scrollback_offset = self.row0().min(self.scrollback_offset + 1);
197                }
198            } else {
199                self.rows.remove(row0);
200            }
201        }
202    }
203
204    /// Resize the grid
205    pub fn resize(&mut self, rows: usize, cols: usize) {
206        self.size = Size { rows, cols };
207
208        // Adjust row count
209        while self.rows.len() < rows {
210            self.rows.push_back(Row::new(cols));
211        }
212
213        // Resize existing rows to new column count
214        for row in &mut self.rows {
215            row.cells.resize(cols, Cell::default());
216        }
217    }
218
219    /// Extract selected text
220    pub fn get_selected_text(&self, low_x: i32, low_y: i32, high_x: i32, high_y: i32) -> String {
221        let mut contents = String::new();
222        let lines_len = high_y - low_y + 1;
223
224        for i in 0..lines_len {
225            let row_idx = (self.row0() as i32 + low_y + i) as usize;
226
227            if let Some(row) = self.rows.get(row_idx) {
228                let start = if i == 0 { low_x.max(0) as usize } else { 0 };
229                let width = if i == lines_len - 1 {
230                    (high_x + 1 - start as i32).max(0) as usize
231                } else {
232                    row.cols().saturating_sub(start)
233                };
234
235                let text = row.text_content(start, width);
236                contents.push_str(&text);
237
238                // Add newline unless it's the last line or row is wrapped
239                if i != lines_len - 1 && !row.wrapped() {
240                    contents.push('\n');
241                }
242            }
243        }
244
245        contents
246    }
247
248    /// Clear all cells
249    pub fn clear(&mut self) {
250        for row in &mut self.rows {
251            for cell in &mut row.cells {
252                *cell = Cell::default();
253            }
254        }
255    }
256}