vtcode_core/tools/pty/
preview.rs1use portable_pty::PtySize;
2
3use crate::config::PtyConfig;
4
5use super::screen_backend::PtyScreenState;
6use super::scrollback::PtyScrollback;
7
8pub 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}