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 {
46 self.hardware_cursor_row
47 }
48
49 pub fn set_hardware_cursor_row(&mut self, row: usize) {
52 self.hardware_cursor_row = row;
53 }
54
55 pub(crate) fn extract_cursor_marker(
59 &self,
60 lines: &mut [String],
61 height: usize,
62 ) -> Option<(usize, usize)> {
63 let viewport_top = lines.len().saturating_sub(height);
64 for row in (viewport_top..lines.len()).rev() {
65 let line = &lines[row];
66 if let Some(marker_idx) = line.find(CURSOR_MARKER) {
67 use crate::tui::util::visible_width;
68 let col = visible_width(&line[..marker_idx]);
69 let before = &line[..marker_idx];
71 let after = &line[marker_idx + CURSOR_MARKER.len()..];
72 lines[row] = format!("{}{}", before, after);
73 return Some((row, col));
74 }
75 }
76 None
77 }
78
79 pub fn prev_width(&self) -> usize {
80 self.prev_width as usize
81 }
82
83 pub fn prev_height(&self) -> usize {
84 self.prev_height as usize
85 }
86
87 pub fn full_redraw_count(&self) -> usize {
88 self.full_redraw_count
89 }
90
91 pub fn total_lines(&self) -> usize {
93 self.prev_lines.len()
94 }
95
96 pub fn finalize(&mut self, writer: &mut dyn Write) -> io::Result<()> {
100 if self.prev_lines.is_empty() {
101 return Ok(());
102 }
103 let target_row = self.prev_lines.len(); let line_diff = target_row as i64 - self.hardware_cursor_row as i64;
105 let mut buf = String::new();
106 if line_diff > 0 {
107 buf.push_str(&format!("\x1b[{}B", line_diff));
108 } else if line_diff < 0 {
109 buf.push_str(&format!("\x1b[{}A", -line_diff));
110 }
111 buf.push_str("\r\n");
112 write!(writer, "{}", buf)?;
113 writer.flush()?;
114 Ok(())
115 }
116
117 pub fn set_clear_on_shrink(&mut self, enabled: bool) {
118 self.clear_on_shrink = enabled;
119 }
120
121 pub fn set_use_sync_output(&mut self, enabled: bool) {
124 self.use_sync_output = enabled;
125 }
126
127 fn sync_begin(&self, buf: &mut String) {
129 if self.use_sync_output {
130 buf.push_str("\x1b[?2026h");
131 }
132 }
133
134 fn sync_end(&self, buf: &mut String) {
136 if self.use_sync_output {
137 buf.push_str("\x1b[?2026l");
138 }
139 }
140
141 fn full_render(
142 &mut self,
143 lines: &[String],
144 w: &mut dyn Write,
145 clear: bool,
146 width: usize,
147 height: usize,
148 ) -> io::Result<()> {
149 self.full_redraw_count += 1;
150 let mut buf = String::new();
151
152 if clear {
153 buf.push_str("\x1b[2J\x1b[H\x1b[3J");
154 }
155
156 if lines.is_empty() {
157 self.sync_begin(&mut buf);
158 self.sync_end(&mut buf);
159 write!(w, "{}", buf)?;
160 w.flush()?;
161 self.cursor_row = 0;
162 self.hardware_cursor_row = 0;
163 self.max_lines_rendered = 0;
164 self.prev_viewport_top = 0;
165 self.prev_lines = lines.to_vec();
166 self.prev_width = width as u16;
167 self.prev_height = height as u16;
168 return Ok(());
169 }
170
171 self.sync_begin(&mut buf);
172
173 for (i, line) in lines.iter().enumerate() {
174 if i > 0 {
175 buf.push_str("\r\n");
176 }
177 buf.push_str(line);
178 }
179
180 self.sync_end(&mut buf);
181 write!(w, "{}", buf)?;
182 w.flush()?;
183
184 self.cursor_row = lines.len().saturating_sub(1);
185 self.hardware_cursor_row = self.cursor_row;
186 if clear {
187 self.max_lines_rendered = lines.len();
188 } else {
189 self.max_lines_rendered = self.max_lines_rendered.max(lines.len());
190 }
191 let buffer_len = height.max(lines.len());
192 self.prev_viewport_top = buffer_len.saturating_sub(height);
193 self.prev_lines = lines.to_vec();
194 self.prev_width = width as u16;
195 self.prev_height = height as u16;
196
197 Ok(())
198 }
199
200 pub fn render(
208 &mut self,
209 mut new_lines: Vec<String>,
210 width: u16,
211 height: u16,
212 writer: &mut dyn Write,
213 ) -> io::Result<Option<(usize, usize)>> {
214 let width_usize = width as usize;
215 let height_usize = height as usize;
216
217 let cursor_pos = self.extract_cursor_marker(&mut new_lines, height_usize);
219
220 let width_changed = self.prev_width != 0 && self.prev_width as usize != width_usize;
221 let height_changed = self.prev_height != 0 && self.prev_height as usize != height_usize;
222 let prev_buffer_len = if self.prev_height > 0 {
223 self.prev_viewport_top + self.prev_height as usize
224 } else {
225 height_usize
226 };
227 let prev_viewport_top = if height_changed {
228 prev_buffer_len.saturating_sub(height_usize)
229 } else {
230 self.prev_viewport_top
231 };
232 let mut viewport_top = prev_viewport_top;
233
234 if self.prev_lines.is_empty() && !width_changed && !height_changed {
236 self.full_render(&new_lines, writer, false, width_usize, height_usize)?;
237 return Ok(cursor_pos);
238 }
239
240 if width_changed || height_changed {
242 self.full_render(&new_lines, writer, true, width_usize, height_usize)?;
243 return Ok(cursor_pos);
244 }
245
246 if self.clear_on_shrink && new_lines.len() < self.max_lines_rendered {
248 self.full_render(&new_lines, writer, true, width_usize, height_usize)?;
249 return Ok(cursor_pos);
250 }
251
252 let mut first_changed: i32 = -1;
254 let mut last_changed: i32 = -1;
255 let max_lines = new_lines.len().max(self.prev_lines.len());
256 for i in 0..max_lines {
257 let old = if i < self.prev_lines.len() {
258 &self.prev_lines[i]
259 } else {
260 ""
261 };
262 let new = if i < new_lines.len() {
263 &new_lines[i]
264 } else {
265 ""
266 };
267 if old != new {
268 if first_changed == -1 {
269 first_changed = i as i32;
270 }
271 last_changed = i as i32;
272 }
273 }
274
275 let appended = new_lines.len() > self.prev_lines.len();
276 if appended && first_changed == -1 {
277 first_changed = self.prev_lines.len() as i32;
278 last_changed = new_lines.len() as i32 - 1;
279 }
280
281 if first_changed == -1 {
283 self.prev_height = height_usize as u16;
284 self.prev_viewport_top = prev_viewport_top;
285 return Ok(cursor_pos);
286 }
287
288 let first = first_changed as usize;
290 let last = last_changed as usize;
291 if first >= new_lines.len() {
292 let mut buf = String::new();
293
294 let target_row = new_lines.len().saturating_sub(1);
296 let line_diff = if target_row >= prev_viewport_top {
297 (target_row - prev_viewport_top) as i32
298 - (self.hardware_cursor_row.saturating_sub(prev_viewport_top)) as i32
299 } else {
300 self.full_render(&new_lines, writer, true, width_usize, height_usize)?;
302 return Ok(cursor_pos);
303 };
304
305 self.sync_begin(&mut buf);
306
307 if line_diff > 0 {
308 buf.push_str(&format!("\x1b[{}B", line_diff));
309 } else if line_diff < 0 {
310 buf.push_str(&format!("\x1b[{}A", -line_diff));
311 }
312 buf.push('\r');
313
314 let extra = self.prev_lines.len().saturating_sub(new_lines.len());
316 if extra > height_usize {
317 self.full_render(&new_lines, writer, true, width_usize, height_usize)?;
318 return Ok(cursor_pos);
319 }
320 if extra > 0 && !new_lines.is_empty() {
321 buf.push_str("\x1b[1B");
322 }
323 for i in 0..extra {
324 buf.push_str("\r\x1b[2K");
325 if i + 1 < extra {
326 buf.push_str("\x1b[1B");
327 }
328 }
329 let move_back = extra.saturating_sub(1) + if new_lines.is_empty() { 0 } else { 1 };
330 if move_back > 0 {
331 buf.push_str(&format!("\x1b[{}A", move_back));
332 }
333
334 self.sync_end(&mut buf);
335 write!(writer, "{}", buf)?;
336 writer.flush()?;
337
338 self.cursor_row = target_row;
339 self.hardware_cursor_row = target_row;
341 self.prev_lines = new_lines;
342 self.prev_viewport_top = prev_viewport_top;
343 self.prev_height = height_usize as u16;
344 return Ok(cursor_pos);
345 }
346
347 if first < prev_viewport_top {
349 self.full_render(&new_lines, writer, true, width_usize, height_usize)?;
350 return Ok(cursor_pos);
351 }
352
353 let mut buf = String::new();
355 self.sync_begin(&mut buf);
356
357 let move_target = if appended && first == self.prev_lines.len() && first > 0 {
358 first - 1
359 } else {
360 first
361 };
362
363 let prev_viewport_bottom = prev_viewport_top + height_usize - 1;
365 if move_target > prev_viewport_bottom {
366 let scroll = move_target - prev_viewport_bottom;
367 let current_screen_row =
369 (self.hardware_cursor_row.saturating_sub(prev_viewport_top)).min(height_usize - 1);
370 let to_bottom = height_usize - 1 - current_screen_row;
371 if to_bottom > 0 {
372 buf.push_str(&format!("\x1b[{}B", to_bottom));
373 }
374 for _ in 0..scroll {
376 buf.push_str("\r\n");
377 }
378 self.hardware_cursor_row = move_target;
379 viewport_top += scroll;
381 }
382
383 let current_screen_row = self.hardware_cursor_row.saturating_sub(viewport_top);
387 let target_screen_row = move_target.saturating_sub(viewport_top);
388 let line_diff = target_screen_row as i32 - current_screen_row as i32;
389
390 if line_diff > 0 {
391 buf.push_str(&format!("\x1b[{}B", line_diff));
392 } else if line_diff < 0 {
393 buf.push_str(&format!("\x1b[{}A", -line_diff));
394 }
395
396 if appended && first == self.prev_lines.len() {
397 buf.push_str("\r\n");
398 } else {
399 buf.push('\r');
400 }
401
402 let render_end = last.min(new_lines.len() - 1);
404 for (i, line) in new_lines
405 .iter()
406 .enumerate()
407 .skip(first)
408 .take(render_end + 1 - first)
409 {
410 if i > first {
411 buf.push_str("\r\n");
412 }
413
414 let line_without_marker = if line.contains(CURSOR_MARKER) {
416 line.replace(CURSOR_MARKER, "")
417 } else {
418 line.clone()
419 };
420
421 buf.push_str("\x1b[2K"); buf.push_str(&line_without_marker);
423 }
424
425 if new_lines.len() < self.prev_lines.len() {
430 let extra = self.prev_lines.len() - new_lines.len();
431
432 if extra > height_usize {
433 self.sync_end(&mut buf);
435 write!(writer, "{}", buf)?;
436 writer.flush()?;
437 self.full_render(&new_lines, writer, true, width_usize, height_usize)?;
438 return Ok(cursor_pos);
439 }
440
441 let move_to_first_extra = new_lines.len() - render_end;
443 if move_to_first_extra > 0 {
444 buf.push_str(&format!("\x1b[{}B", move_to_first_extra));
445 }
446
447 for i in 0..extra {
449 buf.push_str("\r\x1b[2K");
450 if i + 1 < extra {
451 buf.push_str("\x1b[1B");
452 }
453 }
454
455 if extra > 0 {
458 buf.push_str(&format!("\x1b[{}A", extra));
459 }
460 }
461
462 self.sync_end(&mut buf);
463 write!(writer, "{}", buf)?;
464 writer.flush()?;
465
466 let final_cursor_row = if new_lines.len() < self.prev_lines.len() {
470 new_lines.len().saturating_sub(1)
471 } else {
472 render_end
473 };
474 self.cursor_row = final_cursor_row;
475 self.hardware_cursor_row = final_cursor_row;
476 self.max_lines_rendered = self.max_lines_rendered.max(new_lines.len());
477 self.prev_lines = new_lines;
478 let hw_row_for_viewport = final_cursor_row;
481 self.prev_viewport_top =
482 viewport_top.max(hw_row_for_viewport.saturating_sub(height_usize - 1));
483 self.prev_height = height_usize as u16;
484 self.prev_width = width_usize as u16;
485
486 Ok(cursor_pos)
487 }
488}
489
490impl Default for Screen {
491 fn default() -> Self {
492 Self::new()
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn test_new_screen() {
502 let screen = Screen::new();
503 assert_eq!(screen.full_redraw_count(), 0);
504 }
505
506 #[test]
507 fn test_clear_on_shrink_default() {
508 let screen = Screen::new();
509 assert!(screen.clear_on_shrink);
510 }
511
512 #[test]
513 fn test_first_render() {
514 let mut screen = Screen::new();
515 let lines = vec!["hello".to_string(), "world".to_string()];
516 let mut output = Vec::new();
517
518 screen.render(lines.clone(), 80, 24, &mut output).unwrap();
519
520 let output_str = String::from_utf8(output).unwrap();
521 assert!(output_str.contains("hello"));
522 assert!(output_str.contains("world"));
523 }
524
525 #[test]
526 fn test_differential_update() {
527 let mut screen = Screen::new();
528 let mut output = Vec::new();
529
530 let lines1 = vec!["hello".to_string(), "world".to_string()];
532 screen.render(lines1.clone(), 80, 24, &mut output).unwrap();
533 output.clear();
534
535 screen.render(lines1.clone(), 80, 24, &mut output).unwrap();
537 assert!(output.is_empty());
538
539 let lines2 = vec!["hello".to_string(), "rust".to_string()];
541 screen.render(lines2.clone(), 80, 24, &mut output).unwrap();
542 let output_str = String::from_utf8(output.clone()).unwrap();
543 assert!(output_str.contains("rust"));
544 }
545
546 #[test]
547 fn test_type_character_single_line_change() {
548 let mut screen = Screen::new();
549 let mut output = Vec::new();
550
551 let mut initial: Vec<String> = Vec::new();
553 for i in 0..12 {
554 initial.push(format!("line {:02}", i));
555 }
556 screen.render(initial.clone(), 40, 24, &mut output).unwrap();
557 output.clear();
558
559 let mut after = initial.clone();
561 after[7] = "line 07/".to_string();
562 screen.render(after, 40, 24, &mut output).unwrap();
563
564 let text = String::from_utf8_lossy(&output);
565 assert!(
567 text.contains("line 07/"),
568 "Missing changed text in: {}",
569 text
570 );
571 assert!(
573 !text.contains("\x1b[2J"),
574 "Should not full-clear on single line change"
575 );
576 }
577
578 #[test]
579 fn test_screen_append_no_duplicate_content() {
580 let mut screen = Screen::new();
581 let mut output = Vec::new();
582
583 let frame1 = vec!["a", "b", "c", "d"]
585 .into_iter()
586 .map(String::from)
587 .collect::<Vec<_>>();
588 screen.render(frame1, 40, 24, &mut output).unwrap();
589 output.clear();
590
591 let frame2 = vec!["a", "b", "c", "d", "e"]
593 .into_iter()
594 .map(String::from)
595 .collect::<Vec<_>>();
596 screen.render(frame2, 40, 24, &mut output).unwrap();
597
598 let content = String::from_utf8_lossy(&output);
599 eprintln!("Append-only diff output: {:?}", content);
600
601 let counts = ["a", "b", "c", "d"];
604 for &ch in &counts {
605 let n = content.matches(ch).count();
606 assert!(
607 n <= 1,
608 "'{}' should appear at most once in diff, got {}: {:?}",
609 ch,
610 n,
611 content
612 );
613 }
614 let e_count = content.matches('e').count();
616 assert_eq!(
617 e_count, 1,
618 "'e' should appear exactly once, got {}",
619 e_count
620 );
621 }
622
623 #[test]
624 fn test_screen_insert_line_mid_content_no_duplicates() {
625 let mut screen = Screen::new();
626 let mut output = Vec::new();
627
628 let frame1 = vec!["a", "c", "d"]
630 .into_iter()
631 .map(String::from)
632 .collect::<Vec<_>>();
633 screen.render(frame1, 40, 24, &mut output).unwrap();
634 output.clear();
635
636 let frame2 = vec!["a", "b", "c", "d"]
638 .into_iter()
639 .map(String::from)
640 .collect::<Vec<_>>();
641 screen.render(frame2, 40, 24, &mut output).unwrap();
642
643 let content = String::from_utf8_lossy(&output);
644 eprintln!("Insert-mid diff output: {:?}", content);
645
646 assert!(
648 content.matches('a').count() <= 1,
649 "'a' should appear at most once: {:?}",
650 content
651 );
652 assert!(content.contains('b'), "Should contain 'b'");
654 assert!(content.contains('c'), "Should contain 'c'");
655 assert!(content.contains('d'), "Should contain 'd'");
656 }
657
658 #[test]
659 fn test_screen_editor_appended_empty_line_no_duplicate() {
660 let mut screen = Screen::new();
664 let mut output = Vec::new();
665
666 let frame1 = vec![
667 "header".to_string(),
668 "── editor border ──".to_string(),
669 "hello".to_string(),
670 "── editor border ──".to_string(),
671 "footer".to_string(),
672 ];
673 screen.render(frame1, 30, 24, &mut output).unwrap();
674 output.clear();
675
676 let frame2 = vec![
678 "header".to_string(),
679 "── editor border ──".to_string(),
680 "hello".to_string(),
681 "".to_string(), "── editor border ──".to_string(),
683 "footer".to_string(),
684 ];
685 screen.render(frame2, 30, 24, &mut output).unwrap();
686
687 let content = String::from_utf8_lossy(&output);
688 eprintln!("Editor append empty line diff: {:?}", content);
689
690 let hello_count = content.matches("hello").count();
692 assert!(
693 hello_count <= 1,
694 "'hello' should appear at most once in diff, got {}: {:?}",
695 hello_count,
696 content
697 );
698 let footer_count = content.matches("footer").count();
700 assert!(
701 footer_count <= 1,
702 "'footer' should appear at most once in diff, got {}: {:?}",
703 footer_count,
704 content
705 );
706 }
707
708 #[test]
709 fn test_hardware_cursor_row_after_full_render() {
710 let mut screen = Screen::new();
711 let mut output = Vec::new();
712
713 let lines = vec!["a", "b", "c", "d"]
714 .into_iter()
715 .map(String::from)
716 .collect::<Vec<_>>();
717 screen.render(lines, 40, 24, &mut output).unwrap();
718
719 assert_eq!(screen.hardware_cursor_row(), 3);
721 }
722
723 #[test]
724 fn test_hardware_cursor_row_after_diff_single_line() {
725 let mut screen = Screen::new();
726 let mut output = Vec::new();
727
728 let initial: Vec<String> = (0..6).map(|i| format!("line {}", i)).collect();
729 screen.render(initial.clone(), 40, 24, &mut output).unwrap();
730 output.clear();
731
732 let mut changed = initial.clone();
734 changed[2] = "line 2 modified".to_string();
735 screen.render(changed, 40, 24, &mut output).unwrap();
736
737 assert_eq!(screen.hardware_cursor_row(), 2);
739 }
740
741 #[test]
742 fn test_hardware_cursor_row_after_diff_content_shrunk() {
743 let mut screen = Screen::new();
744 let mut output = Vec::new();
745
746 let initial: Vec<String> = (0..6).map(|i| format!("line {}", i)).collect();
747 screen.render(initial, 40, 24, &mut output).unwrap();
748 output.clear();
749
750 let after: Vec<String> = (0..4).map(|i| format!("line {}", i)).collect();
752 screen.render(after, 40, 24, &mut output).unwrap();
753
754 assert_eq!(screen.hardware_cursor_row(), 3);
756 }
757
758 #[test]
759 fn test_set_hardware_cursor_row_syncs_tracking() {
760 let mut screen = Screen::new();
761 let mut output = Vec::new();
762
763 let lines = vec!["a", "b", "c"]
764 .into_iter()
765 .map(String::from)
766 .collect::<Vec<_>>();
767 screen.render(lines, 40, 24, &mut output).unwrap();
768 assert_eq!(screen.hardware_cursor_row(), 2);
769
770 screen.set_hardware_cursor_row(0);
772 assert_eq!(screen.hardware_cursor_row(), 0);
773
774 output.clear();
776 let new_lines = vec!["changed", "b", "c"]
777 .into_iter()
778 .map(String::from)
779 .collect::<Vec<_>>();
780 screen.render(new_lines, 40, 24, &mut output).unwrap();
781
782 let diff = String::from_utf8_lossy(&output);
784 assert!(diff.contains("changed"), "Diff should contain changed line");
786 }
787}