rskit_cli/live/screen/
region.rs1use super::perform::Performer;
10
11pub struct RegionScreen {
20 parser: vte::Parser,
21 performer: Performer,
22}
23
24impl std::fmt::Debug for RegionScreen {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_struct("RegionScreen")
28 .field("performer", &self.performer)
29 .finish_non_exhaustive()
30 }
31}
32
33impl RegionScreen {
34 #[must_use]
36 pub fn new(rows: usize, cols: usize) -> Self {
37 Self {
38 parser: vte::Parser::new(),
39 performer: Performer::new(rows.max(1), cols.max(1)),
40 }
41 }
42
43 pub fn feed(&mut self, bytes: &[u8]) -> Vec<String> {
50 self.parser.advance(&mut self.performer, bytes);
51 self.performer.take_evicted()
52 }
53
54 #[must_use]
56 pub fn render(&self) -> Vec<String> {
57 self.performer.render()
58 }
59
60 #[must_use]
63 pub fn drain(self) -> Vec<String> {
64 let mut rows = self.performer.render();
65 while rows.last().is_some_and(String::is_empty) {
66 rows.pop();
67 }
68 rows
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::RegionScreen;
75
76 #[test]
77 fn carriage_return_progress_collapses() {
78 let mut screen = RegionScreen::new(1, 8);
79 assert!(screen.feed(b"50%\r100%").is_empty());
80 assert_eq!(screen.render(), vec!["100%".to_string()]);
81 }
82
83 #[test]
84 fn utf8_split_across_feeds_is_reassembled() {
85 let mut screen = RegionScreen::new(1, 4);
86 assert!(screen.feed(&[0xC3]).is_empty());
87 assert!(screen.feed(&[0xA9]).is_empty());
88 assert_eq!(screen.render(), vec!["é".to_string()]);
89 }
90
91 #[test]
92 fn in_place_multiline_redraw_updates_block_without_duplication() {
93 let mut screen = RegionScreen::new(3, 8);
94 screen.feed(b"a\r\nb\r\nc");
95 screen.feed(b"\x1b[H\x1b[2Jx\r\ny\r\nz");
97 assert_eq!(
98 screen.render(),
99 vec!["x".to_string(), "y".to_string(), "z".to_string()]
100 );
101 }
102
103 #[test]
104 fn regression_cargo_style_redraw_leaks_no_control_bytes() {
105 let mut screen = RegionScreen::new(6, 20);
106 let frame = b"Compiling\r\n\x1b[7A\r\x1b[2K\x1b[1BChecking\r\nDone";
110 let evicted = screen.feed(frame);
111 let mut rendered = evicted;
112 rendered.extend(screen.render());
113 for line in &rendered {
114 assert!(!line.contains("\x1b[2K"));
115 assert!(!line.contains("\x1b[7A"));
116 assert!(!line.contains("\x1b[1B"));
117 }
118 }
119
120 #[test]
121 fn evicted_plus_drain_reconstruct_full_transcript() {
122 let mut screen = RegionScreen::new(2, 4);
123 let mut all = Vec::new();
124 all.extend(screen.feed(b"l1\r\nl2\r\nl3\r\nl4\r\n"));
125 all.extend(screen.drain());
126 assert_eq!(all, vec!["l1", "l2", "l3", "l4"]);
127 }
128
129 #[test]
130 fn drain_trims_trailing_blank_rows() {
131 let mut screen = RegionScreen::new(5, 8);
132 screen.feed(b"one\r\ntwo");
133 assert_eq!(screen.drain(), vec!["one".to_string(), "two".to_string()]);
134 }
135}