1use thiserror::Error;
2
3#[derive(Error, Debug, Clone, PartialEq)]
5pub enum RenderError {
6 #[error("width overflow: line width {actual} exceeds {width}")]
8 WidthOverflow {
9 line: String,
11 width: u16,
13 actual: usize,
15 },
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum InputResult {
21 Handled,
23 Ignored,
25 RequestRender,
27}
28
29#[derive(Debug, Clone, PartialEq)]
34pub struct Rendered {
35 pub lines: Vec<String>,
37 pub cursor: Option<(usize, usize)>,
39 pub images: Vec<ImageCommand>,
41}
42
43#[derive(Debug, Clone, PartialEq)]
49pub struct ImageCommand {
50 pub id: u32,
52 pub data: String,
54 pub row: u16,
56 pub col: u16,
58}
59
60impl Rendered {
61 pub fn empty() -> Self {
63 Self {
64 lines: Vec::new(),
65 cursor: None,
66 images: Vec::new(),
67 }
68 }
69
70 pub fn blit_onto(&self, target: &mut Rendered, row: u16, col: u16) {
75 for (i, line) in self.lines.iter().enumerate() {
76 let target_row = row as usize + i;
77 if target_row >= target.lines.len() {
78 break;
79 }
80 let col_usize = col as usize;
81 let target_vw = crate::utils::visible_width(&target.lines[target_row]);
82 if target_vw < col_usize {
84 target.lines[target_row].push_str(&" ".repeat(col_usize - target_vw));
85 }
86 let source_vw = crate::utils::visible_width(line);
87 let end = col_usize + source_vw;
88 let target_vw_after = crate::utils::visible_width(&target.lines[target_row]);
89 if end > target_vw_after {
90 target.lines[target_row].push_str(&" ".repeat(end - target_vw_after));
91 }
92 let start_byte =
93 crate::utils::byte_index_at_visual_pos(&target.lines[target_row], col_usize);
94 let end_byte = crate::utils::byte_index_at_visual_pos(&target.lines[target_row], end);
95 target.lines[target_row].replace_range(start_byte..end_byte, line);
96 }
97 if let Some((r, c)) = self.cursor {
98 target.cursor = Some((row as usize + r, col as usize + c));
99 }
100 for image in &self.images {
101 target.images.push(ImageCommand {
102 id: image.id,
103 data: image.data.clone(),
104 row: row + image.row,
105 col: col + image.col,
106 });
107 }
108 }
109
110 pub fn blit_into_rect(&self, target: &mut Rendered, rect: Rect) {
115 for (i, line) in self.lines.iter().enumerate().take(rect.height as usize) {
116 let target_row = rect.y as usize + i;
117 if target_row >= target.lines.len() {
118 while target.lines.len() <= target_row {
119 target.lines.push(String::new());
120 }
121 }
122 let col = rect.x as usize;
123 let target_line = &mut target.lines[target_row];
124 let target_vw = crate::utils::visible_width(target_line);
125 if target_vw < col {
126 target_line.push_str(&" ".repeat(col - target_vw));
127 }
128 let truncated = if crate::utils::visible_width(line) > rect.width as usize {
129 Some(crate::utils::truncate_to_width(line, rect.width, ""))
130 } else {
131 None
132 };
133 let source = truncated.as_deref().unwrap_or(line);
134 let vw = crate::utils::visible_width(source);
135 let end = col + vw;
136 let target_vw_after = crate::utils::visible_width(target_line);
137 if end > target_vw_after {
138 target_line.push_str(&" ".repeat(end - target_vw_after));
139 }
140 let mut start_byte = crate::utils::byte_index_at_visual_pos(target_line, col);
141 let end_byte = crate::utils::byte_index_at_visual_pos(target_line, end);
142 if target_line.as_bytes().get(start_byte) == Some(&b'\x1b') &&
145 target_line[start_byte..].starts_with("\x1b[0m")
146 {
147 start_byte = (start_byte + "\x1b[0m".len()).min(end_byte);
148 }
149 target_line.replace_range(start_byte..end_byte, source);
150 }
151 if let Some((r, c)) = self.cursor {
152 target.cursor = Some((rect.y as usize + r, rect.x as usize + c));
153 }
154 for image in &self.images {
155 target.images.push(ImageCommand {
156 id: image.id,
157 data: image.data.clone(),
158 row: rect.y + image.row,
159 col: rect.x + image.col,
160 });
161 }
162 }
163}
164
165use std::{
166 collections::HashMap,
167 io,
168};
169
170macro_rules! try_io {
171 ($expr:expr) => {
172 match $expr {
173 | Ok(v) => v,
174 | Err(e) => return Err(e),
175 }
176 };
177}
178
179use crate::{
180 layout::Rect,
181 terminal::Terminal,
182};
183
184fn extract_placement(data: &str) -> Option<String> {
187 data.rfind("\x1b_Ga=p")
188 .map(|start| data[start..].to_string())
189}
190
191fn append_images(
192 buffer: &mut String,
193 images: &[ImageCommand],
194 transmitted: &mut HashMap<u32, String>,
195) {
196 for image in images {
197 buffer.push_str(&format!("\x1b[{};{}H", image.row + 1, image.col + 1));
201 if let Some(placement) = transmitted.get(&image.id) {
205 buffer.push_str(placement);
206 } else {
207 buffer.push_str(&image.data);
208 if let Some(placement) = extract_placement(&image.data) {
209 transmitted.insert(image.id, placement);
210 }
211 }
212 }
213}
214
215impl Renderer {
216 pub fn render(&mut self, term: &mut dyn Terminal, rendered: &Rendered) -> io::Result<()> {
226 match self.strategy {
227 | RenderStrategy::FirstRender => {
228 let mut buffer = String::from("\x1b[?2026h\x1b[22m\x1b[0m\x1b[2J\x1b[H");
229 for (i, line) in rendered.lines.iter().enumerate() {
230 if i > 0 {
231 buffer.push_str("\r\n");
232 }
233 buffer.push_str(line);
234 }
235 append_images(&mut buffer, &rendered.images, &mut self.transmitted_images);
236 buffer.push_str("\x1b[?2026l");
237 try_io!(term.write(&buffer));
238 },
239 | RenderStrategy::FullRedraw => {
240 self.transmitted_images.clear();
243 let mut buffer = String::from("\x1b[?2026h\x1b[22m\x1b[0m\x1b[2J\x1b[H\x1b[3J");
244 for (i, line) in rendered.lines.iter().enumerate() {
245 if i > 0 {
246 buffer.push_str("\r\n");
247 }
248 buffer.push_str(line);
249 }
250 append_images(&mut buffer, &rendered.images, &mut self.transmitted_images);
251 buffer.push_str("\x1b[?2026l");
252 try_io!(term.write(&buffer));
253 },
254 | RenderStrategy::Diff => {
255 if let Some(ref prev) = self.previous {
256 let mut first_diff: Option<usize> = None;
257 let mut last_diff: usize = 0;
258 let max_lines = prev.lines.len().max(rendered.lines.len());
259 for i in 0..max_lines {
260 let old = prev.lines.get(i).map(|s| s.as_str()).unwrap_or("");
261 let new = rendered.lines.get(i).map(|s| s.as_str()).unwrap_or("");
262 if old != new {
263 if first_diff.is_none() {
264 first_diff = Some(i);
265 }
266 last_diff = i;
267 }
268 }
269 let images_changed = prev.images != rendered.images;
270
271 if first_diff.is_some_and(|f| f >= rendered.lines.len()) {
273 if prev.lines.len() > rendered.lines.len() {
274 let mut buffer = String::from("\x1b[?2026h");
275 let target_row = rendered.lines.len().saturating_sub(1);
276 if target_row > 0 {
277 buffer.push_str(&format!("\x1b[{};1H", target_row + 1));
278 }
279 buffer.push('\r');
280 let extra = prev.lines.len() - rendered.lines.len();
281 if extra > 0 {
282 buffer.push_str("\x1b[1B");
283 }
284 for i in 0..extra {
285 buffer.push_str("\r\x1b[22m\x1b[0m\x1b[2K");
286 if i < extra - 1 {
287 buffer.push_str("\x1b[1B");
288 }
289 }
290 if extra > 0 {
291 buffer.push_str(&format!("\x1b[{}A", extra));
292 }
293 append_images(
294 &mut buffer,
295 &rendered.images,
296 &mut self.transmitted_images,
297 );
298 buffer.push_str("\x1b[?2026l");
299 try_io!(term.write(&buffer));
300 }
301 } else if let Some(start) = first_diff {
302 let mut buffer = String::from("\x1b[?2026h");
303 buffer.push_str(&format!("\x1b[{};1H", start + 1));
305 buffer.push('\r');
307
308 let render_end = last_diff.min(rendered.lines.len().saturating_sub(1));
309 for i in start..=render_end {
310 if i > start {
311 buffer.push_str("\r\n");
312 }
313 buffer.push_str("\x1b[22m\x1b[0m\x1b[2K");
314 buffer.push_str(&rendered.lines[i]);
315 }
316
317 if prev.lines.len() > rendered.lines.len() {
319 let extra = prev.lines.len() - rendered.lines.len();
320 for _ in 0..extra {
321 buffer.push_str("\r\n\x1b[22m\x1b[0m\x1b[2K");
322 }
323 if extra > 0 {
325 buffer.push_str(&format!("\x1b[{}A", extra));
326 }
327 }
328
329 append_images(&mut buffer, &rendered.images, &mut self.transmitted_images);
330 buffer.push_str("\x1b[?2026l");
331 try_io!(term.write(&buffer));
332 } else if images_changed {
333 let mut buffer = String::from("\x1b[?2026h");
334 append_images(&mut buffer, &rendered.images, &mut self.transmitted_images);
335 buffer.push_str("\x1b[?2026l");
336 try_io!(term.write(&buffer));
337 }
338 } else {
339 let mut buffer = String::from("\x1b[?2026h");
341 for (i, line) in rendered.lines.iter().enumerate() {
342 if i > 0 {
343 buffer.push_str("\r\n");
344 }
345 buffer.push_str(line);
346 }
347 append_images(&mut buffer, &rendered.images, &mut self.transmitted_images);
348 buffer.push_str("\x1b[?2026l");
349 try_io!(term.write(&buffer));
350 }
351 },
352 }
353
354 if let Some((row, col)) = rendered.cursor {
355 try_io!(term.move_cursor(row as u16, col as u16));
356 }
357
358 self.previous = Some(rendered.clone());
359 self.strategy = RenderStrategy::Diff;
360 Ok(())
361 }
362}
363
364#[derive(Default)]
366pub enum RenderStrategy {
367 #[default]
369 FirstRender,
370 FullRedraw,
372 Diff,
375}
376
377#[derive(Default)]
382pub struct Renderer {
383 previous: Option<Rendered>,
384 strategy: RenderStrategy,
385 transmitted_images: HashMap<u32, String>,
386}
387
388impl Renderer {
389 pub fn new() -> Self {
392 Self {
393 previous: None,
394 strategy: RenderStrategy::FirstRender,
395 transmitted_images: HashMap::new(),
396 }
397 }
398
399 pub fn set_strategy(&mut self, strategy: RenderStrategy) {
401 self.strategy = strategy;
402 }
403
404 pub fn previous(&self) -> Option<&Rendered> {
406 self.previous.as_ref()
407 }
408
409 pub fn forget_image(&mut self, id: u32) {
414 self.transmitted_images.remove(&id);
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::terminal::TestTerminal;
422
423 #[test]
424 fn first_render_strategy() {
425 let mut term = TestTerminal::new(80, 24);
426 let mut renderer = Renderer::new();
427 let rendered = Rendered {
428 lines: vec!["hello".into()],
429 cursor: None,
430 images: vec![ImageCommand {
431 id: 1,
432 data: "img".into(),
433 row: 0,
434 col: 0,
435 }],
436 };
437 renderer.render(&mut term, &rendered).unwrap();
438 let written = term.written().join("");
439 assert!(written.contains("hello"));
440 assert!(written.contains("img"));
441 assert!(written.contains("\x1b[?2026h"));
442 assert!(written.contains("\x1b[H"));
444 assert!(written.contains("\x1b[2J"));
445 assert!(!written.contains("\x1b[2K"));
446 }
447
448 #[test]
449 fn full_redraw_clears_screen() {
450 let mut term = TestTerminal::new(80, 24);
451 let mut renderer = Renderer::new();
452 renderer.set_strategy(RenderStrategy::FullRedraw);
453 let rendered = Rendered {
454 lines: vec!["test".into()],
455 cursor: Some((0, 1)),
456 images: Vec::new(),
457 };
458 renderer.render(&mut term, &rendered).unwrap();
459 assert!(term.cursor_moves().contains(&(0, 1)));
460 let written = term.written().join("");
461 assert!(written.contains("\x1b[2J"));
462 assert!(written.contains("\x1b[3J"));
463 }
464
465 #[test]
466 fn diff_clears_changed_lines() {
467 let mut term = TestTerminal::new(80, 24);
468 let mut renderer = Renderer::new();
469
470 let frame1 = Rendered {
472 lines: vec!["long old line content".into()],
473 cursor: None,
474 images: Vec::new(),
475 };
476 renderer.render(&mut term, &frame1).unwrap();
477
478 renderer.set_strategy(RenderStrategy::Diff);
480 let frame2 = Rendered {
481 lines: vec!["short".into()],
482 cursor: None,
483 images: Vec::new(),
484 };
485 renderer.render(&mut term, &frame2).unwrap();
486
487 let written = term.written().join("");
488 assert!(
489 written.contains("\x1b[2K"),
490 "diff must clear each changed line"
491 );
492 }
493
494 #[test]
495 fn diff_skips_unchanged_lines() {
496 let mut term = TestTerminal::new(80, 24);
497 let mut renderer = Renderer::new();
498
499 let frame1 = Rendered {
500 lines: vec!["a".into(), "b".into(), "c".into()],
501 cursor: None,
502 images: Vec::new(),
503 };
504 renderer.render(&mut term, &frame1).unwrap();
505
506 renderer.set_strategy(RenderStrategy::Diff);
507 let frame2 = Rendered {
508 lines: vec!["a".into(), "B".into(), "c".into()],
509 cursor: None,
510 images: Vec::new(),
511 };
512 renderer.render(&mut term, &frame2).unwrap();
513
514 let written = term.written().join("");
515 assert!(
517 written.contains("\x1b[2;1H"),
518 "cursor should jump to first changed line"
519 );
520 assert!(
522 written.contains("\x1b[2;1H\r\x1b[22m\x1b[0m\x1b[2K"),
523 "should use \\r after positioning"
524 );
525 let after_line2 = written.split("\x1b[2;1H").nth(1).unwrap_or("");
527 assert!(
528 !after_line2.contains("\r\nc"),
529 "should not rewrite unchanged line 3"
530 );
531 }
532
533 #[test]
534 fn diff_no_previous_treats_as_first_render() {
535 let mut term = TestTerminal::new(80, 24);
536 let mut renderer = Renderer::new();
537 renderer.set_strategy(RenderStrategy::Diff);
538 let rendered = Rendered {
539 lines: vec!["test".into()],
540 cursor: None,
541 images: Vec::new(),
542 };
543 renderer.render(&mut term, &rendered).unwrap();
544 let written = term.written().join("");
545 assert!(!written.contains("\x1b[2J"));
547 assert!(written.contains("test"));
548 }
549
550 #[test]
551 fn diff_clears_deleted_lines() {
552 let mut term = TestTerminal::new(80, 24);
553 let mut renderer = Renderer::new();
554
555 let frame1 = Rendered {
556 lines: vec!["a".into(), "b".into(), "c".into()],
557 cursor: None,
558 images: Vec::new(),
559 };
560 renderer.render(&mut term, &frame1).unwrap();
561
562 renderer.set_strategy(RenderStrategy::Diff);
563 let frame2 = Rendered {
564 lines: vec!["a".into()],
565 cursor: None,
566 images: Vec::new(),
567 };
568 renderer.render(&mut term, &frame2).unwrap();
569
570 let written = term.written().join("");
571 assert!(written.contains("\x1b[2K"), "should clear deleted lines");
573 }
574
575 #[test]
576 fn diff_emits_image_commands() {
577 let mut term = TestTerminal::new(80, 24);
578 let mut renderer = Renderer::new();
579
580 let frame1 = Rendered {
581 lines: vec!["a".into()],
582 cursor: None,
583 images: Vec::new(),
584 };
585 renderer.render(&mut term, &frame1).unwrap();
586
587 renderer.set_strategy(RenderStrategy::Diff);
588 let frame2 = Rendered {
589 lines: vec!["a".into()],
590 cursor: None,
591 images: vec![ImageCommand {
592 id: 5,
593 data: "img".into(),
594 row: 0,
595 col: 0,
596 }],
597 };
598 renderer.render(&mut term, &frame2).unwrap();
599
600 let written = term.written().join("");
601 assert!(written.contains("img"), "diff must emit image commands");
602 }
603
604 #[test]
605 fn diff_reuses_transmitted_image_without_payload() {
606 let mut term = TestTerminal::new(80, 24);
607 let mut renderer = Renderer::new();
608
609 let payload = "\x1b_Ga=t,f=100,i=9,q=1,m=0;DATA\x1b\\\x1b_Ga=p,i=9,c=2,r=1,q=1\x1b\\";
610 let frame1 = Rendered {
611 lines: vec!["a".into()],
612 cursor: None,
613 images: vec![ImageCommand {
614 id: 9,
615 data: payload.into(),
616 row: 0,
617 col: 0,
618 }],
619 };
620 renderer.render(&mut term, &frame1).unwrap();
621 let first = term.written().last().map(String::as_str).unwrap_or("");
622 assert!(first.contains("a=t"));
623 assert!(first.contains("a=p"));
624
625 renderer.set_strategy(RenderStrategy::Diff);
626 let frame2 = Rendered {
627 lines: vec!["b".into()],
628 cursor: None,
629 images: vec![ImageCommand {
630 id: 9,
631 data: payload.into(),
632 row: 0,
633 col: 0,
634 }],
635 };
636 renderer.render(&mut term, &frame2).unwrap();
637 let second = term.written().last().map(String::as_str).unwrap_or("");
638 assert!(
639 !second.contains("a=t"),
640 "image payload should not be re-transmitted on subsequent frames"
641 );
642 assert!(
643 second.contains("a=p"),
644 "image placement should still be emitted"
645 );
646 }
647
648 #[test]
649 fn blit_onto_with_images() {
650 let mut target = Rendered {
651 lines: vec!["hello world".into()],
652 cursor: None,
653 images: Vec::new(),
654 };
655 let source = Rendered {
656 lines: vec!["XY".into()],
657 cursor: Some((0, 1)),
658 images: vec![ImageCommand {
659 id: 1,
660 data: "img".into(),
661 row: 0,
662 col: 0,
663 }],
664 };
665 source.blit_onto(&mut target, 0, 6);
666 assert_eq!(target.images.len(), 1);
667 assert_eq!(target.images[0].row, 0);
668 assert_eq!(target.images[0].col, 6);
669 }
670
671 #[test]
672 fn blit_into_rect_basic() {
673 let mut target = Rendered {
674 lines: vec!["hello world".into(), "second line".into()],
675 cursor: None,
676 images: Vec::new(),
677 };
678 let source = Rendered {
679 lines: vec!["XY".into(), "Z".into()],
680 cursor: Some((0, 1)),
681 images: vec![ImageCommand {
682 id: 1,
683 data: "img".into(),
684 row: 0,
685 col: 0,
686 }],
687 };
688 source.blit_into_rect(&mut target, Rect::new(6, 0, 10, 2));
689 assert_eq!(target.lines[0], "hello XYrld");
690 assert_eq!(target.lines[1], "secondZline");
691 assert_eq!(target.cursor, Some((0, 7)));
692 assert_eq!(target.images.len(), 1);
693 assert_eq!(target.images[0].row, 0);
694 assert_eq!(target.images[0].col, 6);
695 }
696
697 #[test]
698 fn blit_into_rect_clips_height() {
699 let mut target = Rendered {
700 lines: vec!["aaaaaaaaaa".into()],
701 cursor: None,
702 images: Vec::new(),
703 };
704 let source = Rendered {
705 lines: vec!["1".into(), "2".into(), "3".into()],
706 cursor: None,
707 images: Vec::new(),
708 };
709 source.blit_into_rect(&mut target, Rect::new(0, 0, 10, 1));
710 assert_eq!(target.lines[0], "1aaaaaaaaa");
711 assert_eq!(target.lines.len(), 1);
712 }
713
714 #[test]
715 fn blit_into_rect_clips_width() {
716 let mut target = Rendered {
717 lines: vec!["aaaaaaaaaa".into()],
718 cursor: None,
719 images: Vec::new(),
720 };
721 let source = Rendered {
722 lines: vec!["1234567890ABCDEF".into()],
723 cursor: None,
724 images: Vec::new(),
725 };
726 source.blit_into_rect(&mut target, Rect::new(0, 0, 5, 1));
727 assert_eq!(target.lines[0], "12345aaaaa");
728 }
729
730 #[test]
731 fn blit_into_rect_pads_short_target() {
732 let mut target = Rendered {
733 lines: vec!["hi".into()],
734 cursor: None,
735 images: Vec::new(),
736 };
737 let source = Rendered {
738 lines: vec!["XY".into()],
739 cursor: None,
740 images: Vec::new(),
741 };
742 source.blit_into_rect(&mut target, Rect::new(5, 0, 10, 1));
743 assert_eq!(target.lines[0], "hi XY");
744 }
745
746 #[test]
749 fn blit_into_rect_preserves_ansi_reset() {
750 let mut target = Rendered::empty();
751 let source = Rendered {
753 lines: vec!["\x1b[44mhello \x1b[0m".into()],
754 cursor: None,
755 images: Vec::new(),
756 };
757 source.blit_into_rect(&mut target, Rect::new(0, 0, 10, 1));
758 assert!(
760 target.lines[0].contains("\x1b[0m"),
761 "reset code should survive blit"
762 );
763 assert_eq!(crate::utils::visible_width(&target.lines[0]), 10);
765 }
766
767 #[test]
770 fn blit_into_rect_ansi_target() {
771 let mut target = Rendered {
772 lines: vec!["\x1b[31mred text here\x1b[0m".into()],
773 cursor: None,
774 images: Vec::new(),
775 };
776 let source = Rendered {
777 lines: vec!["XY".into()],
778 cursor: None,
779 images: Vec::new(),
780 };
781 source.blit_into_rect(&mut target, Rect::new(4, 0, 10, 1));
783 assert!(target.lines[0].contains("XY"));
784 assert_eq!(crate::utils::visible_width(&target.lines[0]), 13);
785 }
786
787 #[test]
790 fn blit_into_rect_preserves_ansi_reset_at_boundary() {
791 let mut target = Rendered::empty();
792 let blue_box = Rendered {
794 lines: vec!["\x1b[44m \x1b[0m".into()],
795 cursor: None,
796 images: Vec::new(),
797 };
798 blue_box.blit_into_rect(&mut target, Rect::new(0, 0, 8, 1));
799
800 let text = Rendered {
802 lines: vec!["hello".into()],
803 cursor: None,
804 images: Vec::new(),
805 };
806 text.blit_into_rect(&mut target, Rect::new(8, 0, 5, 1));
807
808 assert!(
810 target.lines[0].contains("\x1b[0mhello"),
811 "reset should be preserved before hello: {}",
812 target.lines[0]
813 );
814 assert_eq!(crate::utils::visible_width(&target.lines[0]), 13);
815 }
816
817 #[test]
819 fn blit_onto_ansi_target() {
820 let mut target = Rendered {
821 lines: vec!["\x1b[31mred text\x1b[0m".into()],
822 cursor: None,
823 images: Vec::new(),
824 };
825 let source = Rendered {
826 lines: vec!["XY".into()],
827 cursor: None,
828 images: Vec::new(),
829 };
830 source.blit_onto(&mut target, 0, 4);
832 assert!(target.lines[0].contains("XY"));
833 assert_eq!(crate::utils::visible_width(&target.lines[0]), 8);
834 }
835
836 #[test]
838 fn diff_resets_ansi_before_clear() {
839 let mut term = TestTerminal::new(80, 24);
840 let mut renderer = Renderer::new();
841
842 let frame1 = Rendered {
843 lines: vec!["\x1b[41mred bg\x1b[0m".into()],
844 cursor: None,
845 images: Vec::new(),
846 };
847 renderer.render(&mut term, &frame1).unwrap();
848
849 renderer.set_strategy(RenderStrategy::Diff);
850 let frame2 = Rendered {
851 lines: vec!["plain".into()],
852 cursor: None,
853 images: Vec::new(),
854 };
855 renderer.render(&mut term, &frame2).unwrap();
856
857 let written = term.written().join("");
858 for chunk in written.split("\x1b[2K") {
860 if !chunk.is_empty() && chunk.contains("\x1b[") {
861 assert!(
862 chunk.ends_with("\x1b[22m\x1b[0m") || !chunk.contains("\x1b[2K"),
863 "clear must be preceded by reset: {}",
864 chunk
865 );
866 }
867 }
868 }
869
870 #[test]
872 fn first_render_resets_before_clear() {
873 let mut term = TestTerminal::new(80, 24);
874 let mut renderer = Renderer::new();
875 let rendered = Rendered {
876 lines: vec!["hello".into()],
877 cursor: None,
878 images: Vec::new(),
879 };
880 renderer.render(&mut term, &rendered).unwrap();
881 let written = term.written().join("");
882 assert!(
883 written.contains("\x1b[22m\x1b[0m\x1b[2J"),
884 "reset must precede screen clear"
885 );
886 }
887
888 #[test]
890 fn full_redraw_resets_before_clear() {
891 let mut term = TestTerminal::new(80, 24);
892 let mut renderer = Renderer::new();
893 renderer.set_strategy(RenderStrategy::FullRedraw);
894 let rendered = Rendered {
895 lines: vec!["hello".into()],
896 cursor: None,
897 images: Vec::new(),
898 };
899 renderer.render(&mut term, &rendered).unwrap();
900 let written = term.written().join("");
901 assert!(
902 written.contains("\x1b[22m\x1b[0m\x1b[2J"),
903 "reset must precede screen clear"
904 );
905 }
906}