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
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}