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.lines().map(|line| line.trim_end().to_string()).collect::<Vec<_>>();
52
53    while lines.last().is_some_and(|line| line.trim().is_empty()) {
54        let _ = lines.pop();
55    }
56
57    lines.join("\n")
58}
59
60fn normalize_preview_chunk(chunk: &str) -> String {
61    let mut normalized = String::with_capacity(chunk.len());
62    let mut previous = None;
63
64    for ch in chunk.chars() {
65        if ch == '\n' && previous != Some('\r') {
66            normalized.push('\r');
67        }
68        normalized.push(ch);
69        previous = Some(ch);
70    }
71
72    normalized
73}
74
75#[cfg(test)]
76mod tests {
77    use super::PtyPreviewRenderer;
78    use crate::config::PtyConfig;
79
80    #[test]
81    fn carriage_return_snapshot_keeps_latest_screen_contents() {
82        let mut preview = PtyPreviewRenderer::from_config(&PtyConfig::default());
83        preview.push_str("start\rreplace\n");
84
85        assert_eq!(preview.snapshot_text(), "replace");
86    }
87
88    #[test]
89    fn trims_trailing_blank_screen_rows() {
90        let mut preview = PtyPreviewRenderer::from_config(&PtyConfig::default());
91        preview.push_str("line 1\nline 2\n");
92
93        assert_eq!(preview.snapshot_text(), "line 1\nline 2");
94    }
95
96    #[test]
97    fn ghostty_core_renders_snapshot() {
98        let mut preview = PtyPreviewRenderer::from_config(&PtyConfig::default());
99        preview.push_str("ghostty core\n");
100
101        assert_eq!(preview.snapshot_text(), "ghostty core");
102    }
103}