rust_expect/session/
screen.rs

1//! Screen buffer integration for sessions.
2//!
3//! This module provides integration between sessions and the screen buffer,
4//! allowing for terminal emulation and screen-based operations.
5
6use crate::types::Dimensions;
7
8/// Screen position (row, column).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub struct Position {
11    /// Row (0-indexed).
12    pub row: usize,
13    /// Column (0-indexed).
14    pub col: usize,
15}
16
17impl Position {
18    /// Create a new position.
19    #[must_use]
20    pub const fn new(row: usize, col: usize) -> Self {
21        Self { row, col }
22    }
23}
24
25/// A rectangular region of the screen.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct Region {
28    /// Top-left corner.
29    pub start: Position,
30    /// Bottom-right corner (exclusive).
31    pub end: Position,
32}
33
34impl Region {
35    /// Create a new region.
36    #[must_use]
37    pub const fn new(start: Position, end: Position) -> Self {
38        Self { start, end }
39    }
40
41    /// Create a region from coordinates.
42    #[must_use]
43    pub const fn from_coords(
44        start_row: usize,
45        start_col: usize,
46        end_row: usize,
47        end_col: usize,
48    ) -> Self {
49        Self {
50            start: Position::new(start_row, start_col),
51            end: Position::new(end_row, end_col),
52        }
53    }
54
55    /// Get the width of the region.
56    #[must_use]
57    pub const fn width(&self) -> usize {
58        self.end.col.saturating_sub(self.start.col)
59    }
60
61    /// Get the height of the region.
62    #[must_use]
63    pub const fn height(&self) -> usize {
64        self.end.row.saturating_sub(self.start.row)
65    }
66
67    /// Check if a position is within this region.
68    #[must_use]
69    pub const fn contains(&self, pos: Position) -> bool {
70        pos.row >= self.start.row
71            && pos.row < self.end.row
72            && pos.col >= self.start.col
73            && pos.col < self.end.col
74    }
75}
76
77/// Text attributes for a cell.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
79#[allow(clippy::struct_excessive_bools)]
80pub struct CellAttributes {
81    /// Bold text.
82    pub bold: bool,
83    /// Italic text.
84    pub italic: bool,
85    /// Underlined text.
86    pub underline: bool,
87    /// Blinking text.
88    pub blink: bool,
89    /// Inverse video.
90    pub inverse: bool,
91    /// Hidden text.
92    pub hidden: bool,
93    /// Strikethrough text.
94    pub strikethrough: bool,
95    /// Foreground color (ANSI color code or RGB).
96    pub foreground: Option<Color>,
97    /// Background color (ANSI color code or RGB).
98    pub background: Option<Color>,
99}
100
101/// Color representation.
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum Color {
104    /// ANSI color index (0-255).
105    Indexed(u8),
106    /// RGB color.
107    Rgb(u8, u8, u8),
108}
109
110/// A single cell in the screen buffer.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct Cell {
113    /// The character in this cell.
114    pub char: char,
115    /// Text attributes.
116    pub attrs: CellAttributes,
117    /// Width of the character (1 for normal, 2 for wide chars).
118    pub width: u8,
119}
120
121impl Default for Cell {
122    fn default() -> Self {
123        Self {
124            char: ' ',
125            attrs: CellAttributes::default(),
126            width: 1,
127        }
128    }
129}
130
131/// A simple screen buffer for terminal content.
132///
133/// This provides basic screen buffer functionality. For full terminal
134/// emulation, use the `screen` feature which provides a more complete
135/// implementation.
136pub struct ScreenBuffer {
137    /// Screen cells.
138    cells: Vec<Vec<Cell>>,
139    /// Screen dimensions.
140    dimensions: Dimensions,
141    /// Cursor position.
142    cursor: Position,
143    /// Saved cursor position.
144    saved_cursor: Option<Position>,
145    /// Scroll region.
146    scroll_region: Option<(usize, usize)>,
147}
148
149impl ScreenBuffer {
150    /// Create a new screen buffer.
151    #[must_use]
152    pub fn new(dimensions: Dimensions) -> Self {
153        let rows = dimensions.rows as usize;
154        let cols = dimensions.cols as usize;
155
156        let cells = (0..rows).map(|_| vec![Cell::default(); cols]).collect();
157
158        Self {
159            cells,
160            dimensions,
161            cursor: Position::default(),
162            saved_cursor: None,
163            scroll_region: None,
164        }
165    }
166
167    /// Get the screen dimensions.
168    #[must_use]
169    pub const fn dimensions(&self) -> Dimensions {
170        self.dimensions
171    }
172
173    /// Get the cursor position.
174    #[must_use]
175    pub const fn cursor(&self) -> Position {
176        self.cursor
177    }
178
179    /// Set the cursor position.
180    pub fn set_cursor(&mut self, pos: Position) {
181        self.cursor = Position {
182            row: pos.row.min(self.dimensions.rows as usize - 1),
183            col: pos.col.min(self.dimensions.cols as usize - 1),
184        };
185    }
186
187    /// Move the cursor.
188    pub fn move_cursor(&mut self, rows: isize, cols: isize) {
189        let new_row = (self.cursor.row as isize + rows)
190            .max(0)
191            .min(self.dimensions.rows as isize - 1) as usize;
192        let new_col = (self.cursor.col as isize + cols)
193            .max(0)
194            .min(self.dimensions.cols as isize - 1) as usize;
195        self.cursor = Position::new(new_row, new_col);
196    }
197
198    /// Save the cursor position.
199    pub const fn save_cursor(&mut self) {
200        self.saved_cursor = Some(self.cursor);
201    }
202
203    /// Restore the cursor position.
204    pub const fn restore_cursor(&mut self) {
205        if let Some(pos) = self.saved_cursor {
206            self.cursor = pos;
207        }
208    }
209
210    /// Get a cell at a position.
211    #[must_use]
212    pub fn get(&self, row: usize, col: usize) -> Option<&Cell> {
213        self.cells.get(row).and_then(|r| r.get(col))
214    }
215
216    /// Get a mutable cell at a position.
217    pub fn get_mut(&mut self, row: usize, col: usize) -> Option<&mut Cell> {
218        self.cells.get_mut(row).and_then(|r| r.get_mut(col))
219    }
220
221    /// Put a character at the cursor position.
222    pub fn put_char(&mut self, c: char, attrs: CellAttributes) {
223        if self.cursor.row < self.cells.len() && self.cursor.col < self.cells[0].len() {
224            self.cells[self.cursor.row][self.cursor.col] = Cell {
225                char: c,
226                attrs,
227                width: if c.is_ascii() { 1 } else { 2 },
228            };
229            self.cursor.col += 1;
230            if self.cursor.col >= self.dimensions.cols as usize {
231                self.cursor.col = 0;
232                self.cursor.row += 1;
233            }
234        }
235    }
236
237    /// Get a line as a string.
238    #[must_use]
239    pub fn line(&self, row: usize) -> Option<String> {
240        self.cells.get(row).map(|cells| {
241            cells
242                .iter()
243                .map(|c| c.char)
244                .collect::<String>()
245                .trim_end()
246                .to_string()
247        })
248    }
249
250    /// Get all lines as strings.
251    #[must_use]
252    pub fn lines(&self) -> Vec<String> {
253        (0..self.dimensions.rows as usize)
254            .filter_map(|row| self.line(row))
255            .collect()
256    }
257
258    /// Get the screen content as a single string.
259    #[must_use]
260    pub fn content(&self) -> String {
261        self.lines().join("\n")
262    }
263
264    /// Get text in a region.
265    #[must_use]
266    pub fn region_text(&self, region: Region) -> String {
267        let mut result = String::new();
268        for row in region.start.row..region.end.row.min(self.cells.len()) {
269            if row < self.cells.len() {
270                let start = region.start.col;
271                let end = region.end.col.min(self.cells[row].len());
272                for col in start..end {
273                    result.push(self.cells[row][col].char);
274                }
275                if row < region.end.row - 1 {
276                    result.push('\n');
277                }
278            }
279        }
280        result.trim_end().to_string()
281    }
282
283    /// Clear the screen.
284    pub fn clear(&mut self) {
285        for row in &mut self.cells {
286            for cell in row {
287                *cell = Cell::default();
288            }
289        }
290        self.cursor = Position::default();
291    }
292
293    /// Clear a region.
294    pub fn clear_region(&mut self, region: Region) {
295        for row in region.start.row..region.end.row.min(self.cells.len()) {
296            let start = region.start.col;
297            let end = region.end.col.min(self.cells[row].len());
298            for col in start..end {
299                self.cells[row][col] = Cell::default();
300            }
301        }
302    }
303
304    /// Scroll the screen up by n lines.
305    pub fn scroll_up(&mut self, n: usize) {
306        let (start, end) = self
307            .scroll_region
308            .unwrap_or((0, self.dimensions.rows as usize));
309
310        for _ in 0..n {
311            if start < end && end <= self.cells.len() {
312                self.cells.remove(start);
313                self.cells.insert(
314                    end - 1,
315                    vec![Cell::default(); self.dimensions.cols as usize],
316                );
317            }
318        }
319    }
320
321    /// Scroll the screen down by n lines.
322    pub fn scroll_down(&mut self, n: usize) {
323        let (start, end) = self
324            .scroll_region
325            .unwrap_or((0, self.dimensions.rows as usize));
326
327        for _ in 0..n {
328            if start < end && end <= self.cells.len() {
329                self.cells.remove(end - 1);
330                self.cells
331                    .insert(start, vec![Cell::default(); self.dimensions.cols as usize]);
332            }
333        }
334    }
335
336    /// Set the scroll region.
337    pub const fn set_scroll_region(&mut self, top: usize, bottom: usize) {
338        if top < bottom && bottom <= self.dimensions.rows as usize {
339            self.scroll_region = Some((top, bottom));
340        } else {
341            self.scroll_region = None;
342        }
343    }
344
345    /// Resize the screen.
346    pub fn resize(&mut self, dimensions: Dimensions) {
347        let new_rows = dimensions.rows as usize;
348        let new_cols = dimensions.cols as usize;
349
350        // Resize rows
351        self.cells
352            .resize_with(new_rows, || vec![Cell::default(); new_cols]);
353
354        // Resize columns in each row
355        for row in &mut self.cells {
356            row.resize_with(new_cols, Cell::default);
357        }
358
359        self.dimensions = dimensions;
360
361        // Adjust cursor if necessary
362        self.cursor.row = self.cursor.row.min(new_rows.saturating_sub(1));
363        self.cursor.col = self.cursor.col.min(new_cols.saturating_sub(1));
364    }
365}
366
367impl std::fmt::Debug for ScreenBuffer {
368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        f.debug_struct("ScreenBuffer")
370            .field("dimensions", &self.dimensions)
371            .field("cursor", &self.cursor)
372            .finish()
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn screen_buffer_basic() {
382        let mut screen = ScreenBuffer::new(Dimensions { rows: 24, cols: 80 });
383
384        screen.put_char('H', CellAttributes::default());
385        screen.put_char('i', CellAttributes::default());
386
387        assert_eq!(screen.line(0), Some("Hi".to_string()));
388    }
389
390    #[test]
391    fn screen_buffer_region() {
392        let mut screen = ScreenBuffer::new(Dimensions { rows: 24, cols: 80 });
393
394        for c in "Hello".chars() {
395            screen.put_char(c, CellAttributes::default());
396        }
397
398        let text = screen.region_text(Region::from_coords(0, 0, 1, 5));
399        assert_eq!(text, "Hello");
400    }
401
402    #[test]
403    fn screen_buffer_resize() {
404        let mut screen = ScreenBuffer::new(Dimensions { rows: 24, cols: 80 });
405        screen.resize(Dimensions {
406            rows: 40,
407            cols: 120,
408        });
409
410        assert_eq!(screen.dimensions().rows, 40);
411        assert_eq!(screen.dimensions().cols, 120);
412    }
413
414    #[test]
415    fn position_region() {
416        let region = Region::from_coords(0, 0, 10, 20);
417
418        assert!(region.contains(Position::new(5, 10)));
419        assert!(!region.contains(Position::new(10, 10)));
420        assert!(!region.contains(Position::new(5, 20)));
421
422        assert_eq!(region.width(), 20);
423        assert_eq!(region.height(), 10);
424    }
425}