Skip to main content

vtcode_ghostty_core/
screen.rs

1use crate::cell::Cell;
2use crate::cursor::Cursor;
3use crate::style::Style;
4
5/// Which screen buffer is active.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum ScreenKind {
8    Primary,
9    Alternate,
10}
11
12/// A single screen buffer (grid + cursor + scrollback).
13#[derive(Clone, Debug)]
14pub(crate) struct Screen {
15    pub(crate) grid: Vec<Cell>,
16    pub(crate) cursor: Cursor,
17    pub(crate) saved_cursor: Cursor,
18    pub(crate) pending_wrap: bool,
19    pub(crate) scrollback: Vec<Vec<Cell>>,
20}
21
22impl Screen {
23    pub(crate) fn new(cols: usize, rows: usize, style: Style) -> Self {
24        Self {
25            grid: vec![Cell::blank(style); cols * rows],
26            cursor: Cursor::default(),
27            saved_cursor: Cursor::default(),
28            pending_wrap: false,
29            scrollback: Vec::new(),
30        }
31    }
32
33    pub(crate) fn reset(&mut self, cols: usize, rows: usize, style: Style) {
34        self.grid = vec![Cell::blank(style); cols * rows];
35        self.cursor = Cursor::default();
36        self.saved_cursor = Cursor::default();
37        self.pending_wrap = false;
38        self.scrollback.clear();
39    }
40
41    pub(crate) fn resize(
42        &mut self,
43        old_cols: usize,
44        old_rows: usize,
45        new_cols: usize,
46        new_rows: usize,
47        style: Style,
48    ) {
49        let mut resized = vec![Cell::blank(style); new_cols * new_rows];
50        let copy_cols = old_cols.min(new_cols);
51        let copy_rows = old_rows.min(new_rows);
52
53        for row in 0..copy_rows {
54            let old_start = Self::index(old_cols, 0, row);
55            let new_start = Self::index(new_cols, 0, row);
56            resized[new_start..new_start + copy_cols]
57                .copy_from_slice(&self.grid[old_start..old_start + copy_cols]);
58        }
59
60        self.grid = resized;
61        if new_cols > 0 {
62            self.cursor.col = self.cursor.col.min(new_cols - 1);
63            self.saved_cursor.col = self.saved_cursor.col.min(new_cols - 1);
64        }
65        if new_rows > 0 {
66            self.cursor.row = self.cursor.row.min(new_rows - 1);
67            self.saved_cursor.row = self.saved_cursor.row.min(new_rows - 1);
68        }
69        self.pending_wrap = false;
70    }
71
72    /// Convert (col, row) to a flat grid index.
73    pub(crate) fn index(cols: usize, col: usize, row: usize) -> usize {
74        row * cols + col
75    }
76}
77
78/// Extract plain text from a screen grid, trimming blank rows and trailing spaces.
79pub(crate) fn plain_text_for_screen(screen: &Screen, cols: usize, rows: usize) -> String {
80    let mut last_non_blank_row = None;
81    for row in (0..rows).rev() {
82        if row_end(screen, cols, row) > 0 {
83            last_non_blank_row = Some(row);
84            break;
85        }
86    }
87
88    let Some(last_row) = last_non_blank_row else {
89        return String::new();
90    };
91
92    let mut out = String::new();
93    for row in 0..=last_row {
94        if row > 0 {
95            out.push('\n');
96        }
97        let end = row_end(screen, cols, row);
98        let start = Screen::index(cols, 0, row);
99        push_trimmed_row(&mut out, &screen.grid[start..start + end]);
100    }
101    out
102}
103
104/// Append a row's content to the output string, skipping wide-character continuations.
105pub(crate) fn push_trimmed_row(out: &mut String, row: &[Cell]) {
106    // Find the range of non-blank content
107    let start = row
108        .iter()
109        .position(|cell| !cell.is_blank())
110        .unwrap_or(row.len());
111    let end = row
112        .iter()
113        .rposition(|cell| !cell.is_blank())
114        .map_or(0, |idx| idx + 1);
115
116    if start >= end {
117        return;
118    }
119
120    for cell in &row[start..end] {
121        if !cell.wide_continuation {
122            out.push(cell.ch);
123        }
124    }
125}
126
127fn row_end(screen: &Screen, cols: usize, row: usize) -> usize {
128    let start = Screen::index(cols, 0, row);
129    screen.grid[start..start + cols]
130        .iter()
131        .rposition(|cell| !cell.is_blank())
132        .map_or(0, |col| col + 1)
133}
134
135/// Generate default tab stops (every 8 columns).
136pub(crate) fn default_tab_stops(cols: usize) -> Vec<bool> {
137    let mut stops = vec![false; cols];
138    for col in (8..cols).step_by(8) {
139        stops[col] = true;
140    }
141    stops
142}