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