1use std::io::{self, Write};
2
3use crate::tui::focusable::CURSOR_MARKER;
4
5pub struct Screen {
7 prev_lines: Vec<String>,
8 prev_width: u16,
9 prev_height: u16,
10 cursor_row: usize,
11 hardware_cursor_row: usize,
12 prev_viewport_top: usize,
13 max_lines_rendered: usize,
14 full_redraw_count: usize,
15 clear_on_shrink: bool,
16 use_sync_output: bool,
19}
20
21impl Screen {
22 pub fn new() -> Self {
23 Self {
24 prev_lines: Vec::new(),
25 prev_width: 0,
26 prev_height: 0,
27 cursor_row: 0,
28 hardware_cursor_row: 0,
29 prev_viewport_top: 0,
30 max_lines_rendered: 0,
31 full_redraw_count: 0,
32 clear_on_shrink: true,
33 use_sync_output: true,
34 }
35 }
36
37 pub fn prev_viewport_top(&self) -> usize {
39 self.prev_viewport_top
40 }
41
42 pub fn hardware_cursor_row(&self) -> usize {
44 self.hardware_cursor_row
45 }
46
47 pub fn set_hardware_cursor_row(&mut self, row: usize) {
50 self.hardware_cursor_row = row;
51 }
52
53 pub fn prev_width(&self) -> usize {
54 self.prev_width as usize
55 }
56
57 pub fn prev_height(&self) -> usize {
58 self.prev_height as usize
59 }
60
61 pub fn full_redraw_count(&self) -> usize {
62 self.full_redraw_count
63 }
64
65 pub fn total_lines(&self) -> usize {
67 self.prev_lines.len()
68 }
69
70 pub fn finalize(&mut self, writer: &mut dyn Write) -> io::Result<()> {
74 if self.prev_lines.is_empty() {
75 return Ok(());
76 }
77 let target_row = self.prev_lines.len(); let line_diff = target_row as i64 - self.hardware_cursor_row as i64;
79 let mut buf = String::new();
80 if line_diff > 0 {
81 buf.push_str(&format!("\x1b[{}B", line_diff));
82 } else if line_diff < 0 {
83 buf.push_str(&format!("\x1b[{}A", -line_diff));
84 }
85 buf.push_str("\r\n");
86 write!(writer, "{}", buf)?;
87 writer.flush()?;
88 Ok(())
89 }
90
91 pub fn set_clear_on_shrink(&mut self, enabled: bool) {
92 self.clear_on_shrink = enabled;
93 }
94
95 pub fn set_use_sync_output(&mut self, enabled: bool) {
98 self.use_sync_output = enabled;
99 }
100
101 fn sync_begin(&self, buf: &mut String) {
103 if self.use_sync_output {
104 buf.push_str("\x1b[?2026h");
105 }
106 }
107
108 fn sync_end(&self, buf: &mut String) {
110 if self.use_sync_output {
111 buf.push_str("\x1b[?2026l");
112 }
113 }
114
115 fn full_render(
116 &mut self,
117 lines: &[String],
118 w: &mut dyn Write,
119 clear: bool,
120 width: usize,
121 height: usize,
122 ) -> io::Result<()> {
123 self.full_redraw_count += 1;
124 let mut buf = String::new();
125
126 if clear {
127 buf.push_str("\x1b[2J\x1b[H\x1b[3J");
128 }
129
130 if lines.is_empty() {
131 self.sync_begin(&mut buf);
132 self.sync_end(&mut buf);
133 write!(w, "{}", buf)?;
134 w.flush()?;
135 self.cursor_row = 0;
136 self.hardware_cursor_row = 0;
137 self.max_lines_rendered = 0;
138 self.prev_viewport_top = 0;
139 self.prev_lines = lines.to_vec();
140 self.prev_width = width as u16;
141 self.prev_height = height as u16;
142 return Ok(());
143 }
144
145 self.sync_begin(&mut buf);
146
147 for (i, line) in lines.iter().enumerate() {
148 if i > 0 {
149 buf.push_str("\r\n");
150 }
151 buf.push_str(line);
152 }
153
154 self.sync_end(&mut buf);
155 write!(w, "{}", buf)?;
156 w.flush()?;
157
158 self.cursor_row = lines.len().saturating_sub(1);
159 self.hardware_cursor_row = self.cursor_row;
160 if clear {
161 self.max_lines_rendered = lines.len();
162 } else {
163 self.max_lines_rendered = self.max_lines_rendered.max(lines.len());
164 }
165 let buffer_len = height.max(lines.len());
166 self.prev_viewport_top = buffer_len.saturating_sub(height);
167 self.prev_lines = lines.to_vec();
168 self.prev_width = width as u16;
169 self.prev_height = height as u16;
170
171 Ok(())
172 }
173
174 pub fn render(
178 &mut self,
179 new_lines: Vec<String>,
180 width: u16,
181 height: u16,
182 writer: &mut dyn Write,
183 ) -> io::Result<()> {
184 let width_usize = width as usize;
185 let height_usize = height as usize;
186 let width_changed = self.prev_width != 0 && self.prev_width as usize != width_usize;
187 let height_changed = self.prev_height != 0 && self.prev_height as usize != height_usize;
188 let prev_buffer_len = if self.prev_height > 0 {
189 self.prev_viewport_top + self.prev_height as usize
190 } else {
191 height_usize
192 };
193 let prev_viewport_top = if height_changed {
194 prev_buffer_len.saturating_sub(height_usize)
195 } else {
196 self.prev_viewport_top
197 };
198 let mut viewport_top = prev_viewport_top;
199
200 if self.prev_lines.is_empty() && !width_changed && !height_changed {
202 return self.full_render(&new_lines, writer, false, width_usize, height_usize);
203 }
204
205 if width_changed || height_changed {
207 return self.full_render(&new_lines, writer, true, width_usize, height_usize);
208 }
209
210 if self.clear_on_shrink && new_lines.len() < self.max_lines_rendered {
212 return self.full_render(&new_lines, writer, true, width_usize, height_usize);
213 }
214
215 let mut first_changed: i32 = -1;
217 let mut last_changed: i32 = -1;
218 let max_lines = new_lines.len().max(self.prev_lines.len());
219 for i in 0..max_lines {
220 let old = if i < self.prev_lines.len() {
221 &self.prev_lines[i]
222 } else {
223 ""
224 };
225 let new = if i < new_lines.len() {
226 &new_lines[i]
227 } else {
228 ""
229 };
230 if old != new {
231 if first_changed == -1 {
232 first_changed = i as i32;
233 }
234 last_changed = i as i32;
235 }
236 }
237
238 let appended = new_lines.len() > self.prev_lines.len();
239 if appended && first_changed == -1 {
240 first_changed = self.prev_lines.len() as i32;
241 last_changed = new_lines.len() as i32 - 1;
242 }
243
244 if first_changed == -1 {
246 self.prev_height = height_usize as u16;
247 self.prev_viewport_top = prev_viewport_top;
248 return Ok(());
249 }
250
251 let first = first_changed as usize;
253 let last = last_changed as usize;
254 if first >= new_lines.len() {
255 let mut buf = String::new();
256
257 let target_row = new_lines.len().saturating_sub(1);
259 let line_diff = if target_row >= prev_viewport_top {
260 (target_row - prev_viewport_top) as i32
261 - (self.hardware_cursor_row.saturating_sub(prev_viewport_top)) as i32
262 } else {
263 return self.full_render(&new_lines, writer, true, width_usize, height_usize);
265 };
266
267 self.sync_begin(&mut buf);
268
269 if line_diff > 0 {
270 buf.push_str(&format!("\x1b[{}B", line_diff));
271 } else if line_diff < 0 {
272 buf.push_str(&format!("\x1b[{}A", -line_diff));
273 }
274 buf.push('\r');
275
276 let extra = self.prev_lines.len().saturating_sub(new_lines.len());
278 if extra > height_usize {
279 return self.full_render(&new_lines, writer, true, width_usize, height_usize);
280 }
281 if extra > 0 && !new_lines.is_empty() {
282 buf.push_str("\x1b[1B");
283 }
284 for i in 0..extra {
285 buf.push_str("\r\x1b[2K");
286 if i + 1 < extra {
287 buf.push_str("\x1b[1B");
288 }
289 }
290 let move_back = extra.saturating_sub(1) + if new_lines.is_empty() { 0 } else { 1 };
291 if move_back > 0 {
292 buf.push_str(&format!("\x1b[{}A", move_back));
293 }
294
295 self.sync_end(&mut buf);
296 write!(writer, "{}", buf)?;
297 writer.flush()?;
298
299 self.cursor_row = target_row;
300 self.hardware_cursor_row = target_row;
301 self.prev_lines = new_lines;
302 self.prev_viewport_top = prev_viewport_top;
303 self.prev_height = height_usize as u16;
304 return Ok(());
305 }
306
307 if first < prev_viewport_top {
309 return self.full_render(&new_lines, writer, true, width_usize, height_usize);
310 }
311
312 let mut buf = String::new();
314 self.sync_begin(&mut buf);
315
316 let move_target = if appended && first == self.prev_lines.len() && first > 0 {
317 first - 1
318 } else {
319 first
320 };
321
322 let prev_viewport_bottom = prev_viewport_top + height_usize - 1;
324 if move_target > prev_viewport_bottom {
325 let scroll = move_target - prev_viewport_bottom;
326 let current_screen_row =
328 (self.hardware_cursor_row.saturating_sub(prev_viewport_top)).min(height_usize - 1);
329 let to_bottom = height_usize - 1 - current_screen_row;
330 if to_bottom > 0 {
331 buf.push_str(&format!("\x1b[{}B", to_bottom));
332 }
333 for _ in 0..scroll {
335 buf.push_str("\r\n");
336 }
337 self.hardware_cursor_row = move_target;
338 viewport_top += scroll;
340 }
341
342 let current_screen_row = self.hardware_cursor_row.saturating_sub(viewport_top);
346 let target_screen_row = move_target.saturating_sub(viewport_top);
347 let line_diff = target_screen_row as i32 - current_screen_row as i32;
348
349 if line_diff > 0 {
350 buf.push_str(&format!("\x1b[{}B", line_diff));
351 } else if line_diff < 0 {
352 buf.push_str(&format!("\x1b[{}A", -line_diff));
353 }
354
355 if appended && first == self.prev_lines.len() {
356 buf.push_str("\r\n");
357 } else {
358 buf.push('\r');
359 }
360
361 let mut render_end = last.min(new_lines.len() - 1);
363 for (i, line) in new_lines
364 .iter()
365 .enumerate()
366 .skip(first)
367 .take(render_end + 1 - first)
368 {
369 if i > first {
370 buf.push_str("\r\n");
371 }
372
373 let line_without_marker = if line.contains(CURSOR_MARKER) {
375 line.replace(CURSOR_MARKER, "")
376 } else {
377 line.clone()
378 };
379
380 buf.push_str("\x1b[2K"); buf.push_str(&line_without_marker);
382 }
383
384 if new_lines.len() < self.prev_lines.len() {
389 let extra = self.prev_lines.len() - new_lines.len();
390
391 if extra > height_usize {
392 self.sync_end(&mut buf);
394 write!(writer, "{}", buf)?;
395 writer.flush()?;
396 return self.full_render(&new_lines, writer, true, width_usize, height_usize);
397 }
398
399 let move_to_first_extra = new_lines.len() - render_end;
401 if move_to_first_extra > 0 {
402 buf.push_str(&format!("\x1b[{}B", move_to_first_extra));
403 }
404
405 for i in 0..extra {
407 buf.push_str("\r\x1b[2K");
408 if i + 1 < extra {
409 buf.push_str("\x1b[1B");
410 }
411 }
412
413 if extra > 0 {
417 buf.push_str(&format!("\x1b[{}A", extra));
418 render_end = new_lines.len().saturating_sub(1);
419 }
420 }
421
422 self.sync_end(&mut buf);
423 write!(writer, "{}", buf)?;
424 writer.flush()?;
425
426 self.cursor_row = new_lines.len().saturating_sub(1);
427 self.hardware_cursor_row = render_end;
428 self.max_lines_rendered = self.max_lines_rendered.max(new_lines.len());
429 self.prev_lines = new_lines;
430 self.prev_viewport_top = viewport_top.max(render_end.saturating_sub(height_usize - 1));
433 self.prev_height = height_usize as u16;
434 self.prev_width = width_usize as u16;
435
436 Ok(())
437 }
438}
439
440impl Default for Screen {
441 fn default() -> Self {
442 Self::new()
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn test_new_screen() {
452 let screen = Screen::new();
453 assert_eq!(screen.full_redraw_count(), 0);
454 }
455
456 #[test]
457 fn test_clear_on_shrink_default() {
458 let screen = Screen::new();
459 assert!(screen.clear_on_shrink);
460 }
461
462 #[test]
463 fn test_first_render() {
464 let mut screen = Screen::new();
465 let lines = vec!["hello".to_string(), "world".to_string()];
466 let mut output = Vec::new();
467
468 screen.render(lines.clone(), 80, 24, &mut output).unwrap();
469
470 let output_str = String::from_utf8(output).unwrap();
471 assert!(output_str.contains("hello"));
472 assert!(output_str.contains("world"));
473 }
474
475 #[test]
476 fn test_differential_update() {
477 let mut screen = Screen::new();
478 let mut output = Vec::new();
479
480 let lines1 = vec!["hello".to_string(), "world".to_string()];
482 screen.render(lines1.clone(), 80, 24, &mut output).unwrap();
483 output.clear();
484
485 screen.render(lines1.clone(), 80, 24, &mut output).unwrap();
487 assert!(output.is_empty());
488
489 let lines2 = vec!["hello".to_string(), "rust".to_string()];
491 screen.render(lines2.clone(), 80, 24, &mut output).unwrap();
492 let output_str = String::from_utf8(output.clone()).unwrap();
493 assert!(output_str.contains("rust"));
494 }
495
496 #[test]
497 fn test_type_character_single_line_change() {
498 let mut screen = Screen::new();
499 let mut output = Vec::new();
500
501 let mut initial: Vec<String> = Vec::new();
503 for i in 0..12 {
504 initial.push(format!("line {:02}", i));
505 }
506 screen.render(initial.clone(), 40, 24, &mut output).unwrap();
507 output.clear();
508
509 let mut after = initial.clone();
511 after[7] = "line 07/".to_string();
512 screen.render(after, 40, 24, &mut output).unwrap();
513
514 let text = String::from_utf8_lossy(&output);
515 assert!(
517 text.contains("line 07/"),
518 "Missing changed text in: {}",
519 text
520 );
521 assert!(
523 !text.contains("\x1b[2J"),
524 "Should not full-clear on single line change"
525 );
526 }
527
528 #[test]
529 fn test_screen_append_no_duplicate_content() {
530 let mut screen = Screen::new();
531 let mut output = Vec::new();
532
533 let frame1 = vec!["a", "b", "c", "d"]
535 .into_iter()
536 .map(String::from)
537 .collect::<Vec<_>>();
538 screen.render(frame1, 40, 24, &mut output).unwrap();
539 output.clear();
540
541 let frame2 = vec!["a", "b", "c", "d", "e"]
543 .into_iter()
544 .map(String::from)
545 .collect::<Vec<_>>();
546 screen.render(frame2, 40, 24, &mut output).unwrap();
547
548 let content = String::from_utf8_lossy(&output);
549 eprintln!("Append-only diff output: {:?}", content);
550
551 let counts = ["a", "b", "c", "d"];
554 for &ch in &counts {
555 let n = content.matches(ch).count();
556 assert!(
557 n <= 1,
558 "'{}' should appear at most once in diff, got {}: {:?}",
559 ch,
560 n,
561 content
562 );
563 }
564 let e_count = content.matches('e').count();
566 assert_eq!(
567 e_count, 1,
568 "'e' should appear exactly once, got {}",
569 e_count
570 );
571 }
572
573 #[test]
574 fn test_screen_insert_line_mid_content_no_duplicates() {
575 let mut screen = Screen::new();
576 let mut output = Vec::new();
577
578 let frame1 = vec!["a", "c", "d"]
580 .into_iter()
581 .map(String::from)
582 .collect::<Vec<_>>();
583 screen.render(frame1, 40, 24, &mut output).unwrap();
584 output.clear();
585
586 let frame2 = vec!["a", "b", "c", "d"]
588 .into_iter()
589 .map(String::from)
590 .collect::<Vec<_>>();
591 screen.render(frame2, 40, 24, &mut output).unwrap();
592
593 let content = String::from_utf8_lossy(&output);
594 eprintln!("Insert-mid diff output: {:?}", content);
595
596 assert!(
598 content.matches('a').count() <= 1,
599 "'a' should appear at most once: {:?}",
600 content
601 );
602 assert!(content.contains('b'), "Should contain 'b'");
604 assert!(content.contains('c'), "Should contain 'c'");
605 assert!(content.contains('d'), "Should contain 'd'");
606 }
607
608 #[test]
609 fn test_screen_editor_appended_empty_line_no_duplicate() {
610 let mut screen = Screen::new();
614 let mut output = Vec::new();
615
616 let frame1 = vec![
617 "header".to_string(),
618 "── editor border ──".to_string(),
619 "hello".to_string(),
620 "── editor border ──".to_string(),
621 "footer".to_string(),
622 ];
623 screen.render(frame1, 30, 24, &mut output).unwrap();
624 output.clear();
625
626 let frame2 = vec![
628 "header".to_string(),
629 "── editor border ──".to_string(),
630 "hello".to_string(),
631 "".to_string(), "── editor border ──".to_string(),
633 "footer".to_string(),
634 ];
635 screen.render(frame2, 30, 24, &mut output).unwrap();
636
637 let content = String::from_utf8_lossy(&output);
638 eprintln!("Editor append empty line diff: {:?}", content);
639
640 let hello_count = content.matches("hello").count();
642 assert!(
643 hello_count <= 1,
644 "'hello' should appear at most once in diff, got {}: {:?}",
645 hello_count,
646 content
647 );
648 let footer_count = content.matches("footer").count();
650 assert!(
651 footer_count <= 1,
652 "'footer' should appear at most once in diff, got {}: {:?}",
653 footer_count,
654 content
655 );
656 }
657
658 #[test]
659 fn test_screen_wrapping_no_overwrite_of_unchanged_line() {
660 let mut screen = Screen::new();
665 let mut output = Vec::new();
666
667 let frame1 = vec![
669 "──── editor ────".to_string(),
670 " hello world ".to_string(), "──── editor ────".to_string(),
672 ];
673 screen.render(frame1.clone(), 18, 24, &mut output).unwrap();
674 output.clear();
675
676 screen.set_hardware_cursor_row(1);
680
681 let frame2 = vec![
684 "──── editor ────".to_string(),
685 " hello world ".to_string(), " is ".to_string(), "──── editor ────".to_string(),
688 ];
689 screen.render(frame2.clone(), 18, 24, &mut output).unwrap();
690 eprintln!(
691 "After wrap diff output: {:?}",
692 String::from_utf8_lossy(&output)
693 );
694
695 let output_str = String::from_utf8_lossy(&output);
697 let hw_count = output_str.matches("hello world").count();
701 assert!(
702 hw_count <= 1,
703 "'hello world' should appear at most once in diff, got {}: {:?}",
704 hw_count,
705 output_str
706 );
707
708 output.clear();
709
710 screen.set_hardware_cursor_row(2);
712
713 let frame3 = vec![
715 "──── editor ────".to_string(),
716 " hello world ".to_string(), " iss ".to_string(), "──── editor ────".to_string(),
719 ];
720 screen.render(frame3.clone(), 18, 24, &mut output).unwrap();
721 let output_str2 = String::from_utf8_lossy(&output);
722 eprintln!("After typing diff output: {:?}", output_str2);
723
724 let hw_count2 = output_str2.matches("hello world").count();
726 assert!(
727 hw_count2 <= 1,
728 "'hello world' should NOT be rewritten in diff, got {}: {:?}",
729 hw_count2,
730 output_str2
731 );
732 }
733
734 #[test]
735 fn test_screen_wrapping_overwrite_bug_without_fix() {
736 let mut screen = Screen::new();
739 let mut output = Vec::new();
740
741 let frame1 = vec![
742 "──── editor ────".to_string(),
743 " hello world ".to_string(),
744 "──── editor ────".to_string(),
745 ];
746 screen.render(frame1.clone(), 18, 24, &mut output).unwrap();
747 output.clear();
748
749 let frame2 = vec![
753 "──── editor ────".to_string(),
754 " hello world ".to_string(),
755 " is ".to_string(),
756 "──── editor ────".to_string(),
757 ];
758 screen.render(frame2.clone(), 18, 24, &mut output).unwrap();
759 let output_str = String::from_utf8_lossy(&output);
760 eprintln!("Without fix - after wrap: {:?}", output_str);
761
762 assert_eq!(
775 screen.hardware_cursor_row(),
776 3,
777 "Without fix: hardware_cursor_row should be 3 (bottom border render_end)"
778 );
779
780 output.clear();
781
782 screen.set_hardware_cursor_row(2); let frame3 = vec![
786 "──── editor ────".to_string(),
787 " hello world ".to_string(),
788 " iss ".to_string(),
789 "──── editor ────".to_string(),
790 ];
791 screen.render(frame3.clone(), 18, 24, &mut output).unwrap();
792 let output_str2 = String::from_utf8_lossy(&output);
793 eprintln!("Without fix - after typing: {:?}", output_str2);
794
795 let hw_count = output_str2.matches("hello world").count();
808 assert!(
809 hw_count <= 1,
810 "BUG: 'hello world' appears in frame 3 diff, got {}: {:?}",
811 hw_count,
812 output_str2
813 );
814 }
815}