Skip to main content

vtcode_core/tools/pty/
preview.rs

1use portable_pty::PtySize;
2
3use crate::config::PtyConfig;
4
5use super::screen_backend::PtyScreenState;
6use super::scrollback::PtyScrollback;
7
8/// In-memory PTY preview renderer for inline live previews.
9pub struct PtyPreviewRenderer {
10    screen_state: PtyScreenState,
11    scrollback: PtyScrollback,
12}
13
14impl PtyPreviewRenderer {
15    #[must_use]
16    pub fn from_config(config: &PtyConfig) -> Self {
17        let size = PtySize {
18            rows: config.default_rows,
19            cols: config.default_cols,
20            pixel_width: 0,
21            pixel_height: 0,
22        };
23
24        Self {
25            screen_state: PtyScreenState::new(size, config.scrollback_lines),
26            scrollback: PtyScrollback::new(config.scrollback_lines, config.max_scrollback_bytes),
27        }
28    }
29
30    pub fn push_str(&mut self, chunk: &str) {
31        if chunk.is_empty() {
32            return;
33        }
34
35        let normalized = normalize_preview_chunk(chunk);
36        let bytes = normalized.as_bytes();
37        self.screen_state.process(bytes);
38
39        let mut utf8 = bytes.to_vec();
40        self.scrollback.push_utf8(&mut utf8, false);
41    }
42
43    #[must_use]
44    pub fn snapshot_text(&self) -> String {
45        let snapshot = self.screen_state.prepare_snapshot();
46        normalize_snapshot_text(&snapshot.screen_contents)
47    }
48}
49
50fn normalize_snapshot_text(text: &str) -> String {
51    let mut lines = text
52        .lines()
53        .map(|line| line.trim_end().to_string())
54        .collect::<Vec<_>>();
55
56    while lines.last().is_some_and(|line| line.trim().is_empty()) {
57        let _ = lines.pop();
58    }
59
60    lines.join("\n")
61}
62
63fn normalize_preview_chunk(chunk: &str) -> String {
64    let mut normalized = String::with_capacity(chunk.len());
65    let mut previous = None;
66
67    for ch in chunk.chars() {
68        if ch == '\n' && previous != Some('\r') {
69            normalized.push('\r');
70        }
71        normalized.push(ch);
72        previous = Some(ch);
73    }
74
75    normalized
76}
77
78#[cfg(test)]
79mod tests {
80    use super::PtyPreviewRenderer;
81    use crate::config::PtyConfig;
82
83    #[test]
84    fn carriage_return_snapshot_keeps_latest_screen_contents() {
85        let mut preview = PtyPreviewRenderer::from_config(&PtyConfig::default());
86        preview.push_str("start\rreplace\n");
87
88        assert_eq!(preview.snapshot_text(), "replace");
89    }
90
91    #[test]
92    fn trims_trailing_blank_screen_rows() {
93        let mut preview = PtyPreviewRenderer::from_config(&PtyConfig::default());
94        preview.push_str("line 1\nline 2\n");
95
96        assert_eq!(preview.snapshot_text(), "line 1\nline 2");
97    }
98
99    #[test]
100    fn ghostty_core_renders_snapshot() {
101        let mut preview = PtyPreviewRenderer::from_config(&PtyConfig::default());
102        preview.push_str("ghostty core\n");
103
104        assert_eq!(preview.snapshot_text(), "ghostty core");
105    }
106}