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,
10 width: u16,
11 actual: usize,
12 },
13}
14
15#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum InputResult {
18 Handled,
20 Ignored,
22 RequestRender,
24}
25
26#[derive(Debug, Clone, PartialEq)]
31pub struct Rendered {
32 pub lines: Vec<String>,
34 pub cursor: Option<(usize, usize)>,
36 pub images: Vec<ImageCommand>,
38}
39
40#[derive(Debug, Clone, PartialEq)]
45pub struct ImageCommand {
46 pub id: u32,
48 pub data: String,
50}
51
52impl Rendered {
53 pub fn empty() -> Self {
55 Self {
56 lines: Vec::new(),
57 cursor: None,
58 images: Vec::new(),
59 }
60 }
61
62 pub fn blit_onto(&self, target: &mut Rendered, row: u16, col: u16) {
67 for (i, line) in self.lines.iter().enumerate() {
68 let target_row = row as usize + i;
69 if target_row >= target.lines.len() {
70 break;
71 }
72 let col_usize = col as usize;
73 let target_vw = crate::utils::visible_width(&target.lines[target_row]);
74 if target_vw < col_usize {
76 target.lines[target_row].push_str(&" ".repeat(col_usize - target_vw));
77 }
78 let source_vw = crate::utils::visible_width(line);
79 let end = col_usize + source_vw;
80 let target_vw_after = crate::utils::visible_width(&target.lines[target_row]);
81 if end > target_vw_after {
82 target.lines[target_row].push_str(&" ".repeat(end - target_vw_after));
83 }
84 let start_byte =
85 crate::utils::byte_index_at_visual_pos(&target.lines[target_row], col_usize);
86 let end_byte = crate::utils::byte_index_at_visual_pos(&target.lines[target_row], end);
87 target.lines[target_row].replace_range(start_byte..end_byte, line);
88 }
89 if let Some((r, c)) = self.cursor {
90 target.cursor = Some((row as usize + r, col as usize + c));
91 }
92 target.images.extend(self.images.clone());
93 }
94
95 pub fn blit_into_rect(&self, target: &mut Rendered, rect: Rect) {
100 for (i, line) in self.lines.iter().enumerate().take(rect.height as usize) {
101 let target_row = rect.y as usize + i;
102 if target_row >= target.lines.len() {
103 while target.lines.len() <= target_row {
104 target.lines.push(String::new());
105 }
106 }
107 let col = rect.x as usize;
108 let target_line = &mut target.lines[target_row];
109 let target_vw = crate::utils::visible_width(target_line);
110 if target_vw < col {
111 target_line.push_str(&" ".repeat(col - target_vw));
112 }
113 let truncated = if crate::utils::visible_width(line) > rect.width as usize {
114 Some(crate::utils::truncate_to_width(line, rect.width, ""))
115 } else {
116 None
117 };
118 let source = truncated.as_deref().unwrap_or(line);
119 let vw = crate::utils::visible_width(source);
120 let end = col + vw;
121 let target_vw_after = crate::utils::visible_width(target_line);
122 if end > target_vw_after {
123 target_line.push_str(&" ".repeat(end - target_vw_after));
124 }
125 let mut start_byte = crate::utils::byte_index_at_visual_pos(target_line, col);
126 let end_byte = crate::utils::byte_index_at_visual_pos(target_line, end);
127 if target_line.as_bytes().get(start_byte) == Some(&b'\x1b') &&
130 target_line[start_byte..].starts_with("\x1b[0m")
131 {
132 start_byte = (start_byte + "\x1b[0m".len()).min(end_byte);
133 }
134 target_line.replace_range(start_byte..end_byte, source);
135 }
136 if let Some((r, c)) = self.cursor {
137 target.cursor = Some((rect.y as usize + r, rect.x as usize + c));
138 }
139 target.images.extend(self.images.clone());
140 }
141}
142
143use std::io;
144
145use crate::{
146 layout::Rect,
147 terminal::Terminal,
148};
149
150impl Renderer {
151 pub fn render(&mut self, term: &mut dyn Terminal, rendered: &Rendered) -> io::Result<()> {
161 match self.strategy {
162 | RenderStrategy::FirstRender => {
163 let mut buffer = String::from("\x1b[?2026h\x1b[0m\x1b[2J\x1b[H");
164 for (i, line) in rendered.lines.iter().enumerate() {
165 if i > 0 {
166 buffer.push_str("\r\n");
167 }
168 buffer.push_str(line);
169 }
170 buffer.push_str("\x1b[?2026l");
171 term.write(&buffer)?;
172 },
173 | RenderStrategy::FullRedraw => {
174 let mut buffer = String::from("\x1b[?2026h\x1b[0m\x1b[2J\x1b[H\x1b[3J");
175 for (i, line) in rendered.lines.iter().enumerate() {
176 if i > 0 {
177 buffer.push_str("\r\n");
178 }
179 buffer.push_str(line);
180 }
181 buffer.push_str("\x1b[?2026l");
182 term.write(&buffer)?;
183 },
184 | RenderStrategy::Diff => {
185 if let Some(ref prev) = self.previous {
186 let mut first_diff: Option<usize> = None;
187 let mut last_diff: usize = 0;
188 let max_lines = prev.lines.len().max(rendered.lines.len());
189 for i in 0..max_lines {
190 let old = prev.lines.get(i).map(|s| s.as_str()).unwrap_or("");
191 let new = rendered.lines.get(i).map(|s| s.as_str()).unwrap_or("");
192 if old != new {
193 if first_diff.is_none() {
194 first_diff = Some(i);
195 }
196 last_diff = i;
197 }
198 }
199
200 if first_diff.map_or(false, |f| f >= rendered.lines.len()) {
202 if prev.lines.len() > rendered.lines.len() {
203 let mut buffer = String::from("\x1b[?2026h");
204 let target_row = rendered.lines.len().saturating_sub(1);
205 if target_row > 0 {
206 buffer.push_str(&format!("\x1b[{};1H", target_row + 1));
207 }
208 buffer.push('\r');
209 let extra = prev.lines.len() - rendered.lines.len();
210 if extra > 0 {
211 buffer.push_str("\x1b[1B");
212 }
213 for i in 0..extra {
214 buffer.push_str("\r\x1b[0m\x1b[2K");
215 if i < extra - 1 {
216 buffer.push_str("\x1b[1B");
217 }
218 }
219 if extra > 0 {
220 buffer.push_str(&format!("\x1b[{}A", extra));
221 }
222 buffer.push_str("\x1b[?2026l");
223 term.write(&buffer)?;
224 }
225 } else if let Some(start) = first_diff {
226 let mut buffer = String::from("\x1b[?2026h");
227 buffer.push_str(&format!("\x1b[{};1H", start + 1));
229 buffer.push('\r');
231
232 let render_end = last_diff.min(rendered.lines.len().saturating_sub(1));
233 for i in start..=render_end {
234 if i > start {
235 buffer.push_str("\r\n");
236 }
237 buffer.push_str("\x1b[0m\x1b[2K");
238 buffer.push_str(&rendered.lines[i]);
239 }
240
241 if prev.lines.len() > rendered.lines.len() {
243 let extra = prev.lines.len() - rendered.lines.len();
244 for _ in 0..extra {
245 buffer.push_str("\r\n\x1b[0m\x1b[2K");
246 }
247 if extra > 0 {
249 buffer.push_str(&format!("\x1b[{}A", extra));
250 }
251 }
252
253 buffer.push_str("\x1b[?2026l");
254 term.write(&buffer)?;
255 }
256 } else {
257 let mut buffer = String::from("\x1b[?2026h");
259 for (i, line) in rendered.lines.iter().enumerate() {
260 if i > 0 {
261 buffer.push_str("\r\n");
262 }
263 buffer.push_str(line);
264 }
265 buffer.push_str("\x1b[?2026l");
266 term.write(&buffer)?;
267 }
268 },
269 }
270
271 if let Some((row, col)) = rendered.cursor {
272 term.move_cursor(row as u16, col as u16)?;
273 }
274
275 self.previous = Some(rendered.clone());
276 self.strategy = RenderStrategy::Diff;
277 Ok(())
278 }
279}
280
281pub enum RenderStrategy {
283 FirstRender,
285 FullRedraw,
287 Diff,
290}
291
292pub struct Renderer {
297 previous: Option<Rendered>,
298 strategy: RenderStrategy,
299}
300
301impl Renderer {
302 pub fn new() -> Self {
305 Self {
306 previous: None,
307 strategy: RenderStrategy::FirstRender,
308 }
309 }
310
311 pub fn set_strategy(&mut self, strategy: RenderStrategy) {
313 self.strategy = strategy;
314 }
315
316 pub fn previous(&self) -> Option<&Rendered> {
318 self.previous.as_ref()
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325 use crate::terminal::TestTerminal;
326
327 #[test]
328 fn first_render_strategy() {
329 let mut term = TestTerminal::new(80, 24);
330 let mut renderer = Renderer::new();
331 let rendered = Rendered {
332 lines: vec!["hello".into()],
333 cursor: None,
334 images: vec![ImageCommand {
335 id: 1,
336 data: "img".into(),
337 }],
338 };
339 renderer.render(&mut term, &rendered).unwrap();
340 let written = term.written().join("");
341 assert!(written.contains("hello"));
342 assert!(written.contains("\x1b[?2026h"));
343 assert!(written.contains("\x1b[H"));
345 assert!(written.contains("\x1b[2J"));
346 assert!(!written.contains("\x1b[2K"));
347 }
348
349 #[test]
350 fn full_redraw_clears_screen() {
351 let mut term = TestTerminal::new(80, 24);
352 let mut renderer = Renderer::new();
353 renderer.set_strategy(RenderStrategy::FullRedraw);
354 let rendered = Rendered {
355 lines: vec!["test".into()],
356 cursor: Some((0, 1)),
357 images: Vec::new(),
358 };
359 renderer.render(&mut term, &rendered).unwrap();
360 assert!(term.cursor_moves().contains(&(0, 1)));
361 let written = term.written().join("");
362 assert!(written.contains("\x1b[2J"));
363 assert!(written.contains("\x1b[3J"));
364 }
365
366 #[test]
367 fn diff_clears_changed_lines() {
368 let mut term = TestTerminal::new(80, 24);
369 let mut renderer = Renderer::new();
370
371 let frame1 = Rendered {
373 lines: vec!["long old line content".into()],
374 cursor: None,
375 images: Vec::new(),
376 };
377 renderer.render(&mut term, &frame1).unwrap();
378
379 renderer.set_strategy(RenderStrategy::Diff);
381 let frame2 = Rendered {
382 lines: vec!["short".into()],
383 cursor: None,
384 images: Vec::new(),
385 };
386 renderer.render(&mut term, &frame2).unwrap();
387
388 let written = term.written().join("");
389 assert!(
390 written.contains("\x1b[2K"),
391 "diff must clear each changed line"
392 );
393 }
394
395 #[test]
396 fn diff_skips_unchanged_lines() {
397 let mut term = TestTerminal::new(80, 24);
398 let mut renderer = Renderer::new();
399
400 let frame1 = Rendered {
401 lines: vec!["a".into(), "b".into(), "c".into()],
402 cursor: None,
403 images: Vec::new(),
404 };
405 renderer.render(&mut term, &frame1).unwrap();
406
407 renderer.set_strategy(RenderStrategy::Diff);
408 let frame2 = Rendered {
409 lines: vec!["a".into(), "B".into(), "c".into()],
410 cursor: None,
411 images: Vec::new(),
412 };
413 renderer.render(&mut term, &frame2).unwrap();
414
415 let written = term.written().join("");
416 assert!(
418 written.contains("\x1b[2;1H"),
419 "cursor should jump to first changed line"
420 );
421 assert!(
423 written.contains("\x1b[2;1H\r\x1b[0m\x1b[2K"),
424 "should use \\r after positioning"
425 );
426 let after_line2 = written.split("\x1b[2;1H").nth(1).unwrap_or("");
428 assert!(
429 !after_line2.contains("\r\nc"),
430 "should not rewrite unchanged line 3"
431 );
432 }
433
434 #[test]
435 fn diff_no_previous_treats_as_first_render() {
436 let mut term = TestTerminal::new(80, 24);
437 let mut renderer = Renderer::new();
438 renderer.set_strategy(RenderStrategy::Diff);
439 let rendered = Rendered {
440 lines: vec!["test".into()],
441 cursor: None,
442 images: Vec::new(),
443 };
444 renderer.render(&mut term, &rendered).unwrap();
445 let written = term.written().join("");
446 assert!(!written.contains("\x1b[2J"));
448 assert!(written.contains("test"));
449 }
450
451 #[test]
452 fn diff_clears_deleted_lines() {
453 let mut term = TestTerminal::new(80, 24);
454 let mut renderer = Renderer::new();
455
456 let frame1 = Rendered {
457 lines: vec!["a".into(), "b".into(), "c".into()],
458 cursor: None,
459 images: Vec::new(),
460 };
461 renderer.render(&mut term, &frame1).unwrap();
462
463 renderer.set_strategy(RenderStrategy::Diff);
464 let frame2 = Rendered {
465 lines: vec!["a".into()],
466 cursor: None,
467 images: Vec::new(),
468 };
469 renderer.render(&mut term, &frame2).unwrap();
470
471 let written = term.written().join("");
472 assert!(written.contains("\x1b[2K"), "should clear deleted lines");
474 }
475
476 #[test]
477 fn blit_onto_with_images() {
478 let mut target = Rendered {
479 lines: vec!["hello world".into()],
480 cursor: None,
481 images: Vec::new(),
482 };
483 let source = Rendered {
484 lines: vec!["XY".into()],
485 cursor: Some((0, 1)),
486 images: vec![ImageCommand {
487 id: 1,
488 data: "img".into(),
489 }],
490 };
491 source.blit_onto(&mut target, 0, 6);
492 assert_eq!(target.images.len(), 1);
493 }
494
495 #[test]
496 fn blit_into_rect_basic() {
497 let mut target = Rendered {
498 lines: vec!["hello world".into(), "second line".into()],
499 cursor: None,
500 images: Vec::new(),
501 };
502 let source = Rendered {
503 lines: vec!["XY".into(), "Z".into()],
504 cursor: Some((0, 1)),
505 images: vec![ImageCommand {
506 id: 1,
507 data: "img".into(),
508 }],
509 };
510 source.blit_into_rect(&mut target, Rect::new(6, 0, 10, 2));
511 assert_eq!(target.lines[0], "hello XYrld");
512 assert_eq!(target.lines[1], "secondZline");
513 assert_eq!(target.cursor, Some((0, 7)));
514 assert_eq!(target.images.len(), 1);
515 }
516
517 #[test]
518 fn blit_into_rect_clips_height() {
519 let mut target = Rendered {
520 lines: vec!["aaaaaaaaaa".into()],
521 cursor: None,
522 images: Vec::new(),
523 };
524 let source = Rendered {
525 lines: vec!["1".into(), "2".into(), "3".into()],
526 cursor: None,
527 images: Vec::new(),
528 };
529 source.blit_into_rect(&mut target, Rect::new(0, 0, 10, 1));
530 assert_eq!(target.lines[0], "1aaaaaaaaa");
531 assert_eq!(target.lines.len(), 1);
532 }
533
534 #[test]
535 fn blit_into_rect_clips_width() {
536 let mut target = Rendered {
537 lines: vec!["aaaaaaaaaa".into()],
538 cursor: None,
539 images: Vec::new(),
540 };
541 let source = Rendered {
542 lines: vec!["1234567890ABCDEF".into()],
543 cursor: None,
544 images: Vec::new(),
545 };
546 source.blit_into_rect(&mut target, Rect::new(0, 0, 5, 1));
547 assert_eq!(target.lines[0], "12345aaaaa");
548 }
549
550 #[test]
551 fn blit_into_rect_pads_short_target() {
552 let mut target = Rendered {
553 lines: vec!["hi".into()],
554 cursor: None,
555 images: Vec::new(),
556 };
557 let source = Rendered {
558 lines: vec!["XY".into()],
559 cursor: None,
560 images: Vec::new(),
561 };
562 source.blit_into_rect(&mut target, Rect::new(5, 0, 10, 1));
563 assert_eq!(target.lines[0], "hi XY");
564 }
565
566 #[test]
569 fn blit_into_rect_preserves_ansi_reset() {
570 let mut target = Rendered::empty();
571 let source = Rendered {
573 lines: vec!["\x1b[44mhello \x1b[0m".into()],
574 cursor: None,
575 images: Vec::new(),
576 };
577 source.blit_into_rect(&mut target, Rect::new(0, 0, 10, 1));
578 assert!(
580 target.lines[0].contains("\x1b[0m"),
581 "reset code should survive blit"
582 );
583 assert_eq!(crate::utils::visible_width(&target.lines[0]), 10);
585 }
586
587 #[test]
590 fn blit_into_rect_ansi_target() {
591 let mut target = Rendered {
592 lines: vec!["\x1b[31mred text here\x1b[0m".into()],
593 cursor: None,
594 images: Vec::new(),
595 };
596 let source = Rendered {
597 lines: vec!["XY".into()],
598 cursor: None,
599 images: Vec::new(),
600 };
601 source.blit_into_rect(&mut target, Rect::new(4, 0, 10, 1));
603 assert!(target.lines[0].contains("XY"));
604 assert_eq!(crate::utils::visible_width(&target.lines[0]), 13);
605 }
606
607 #[test]
610 fn blit_into_rect_preserves_ansi_reset_at_boundary() {
611 let mut target = Rendered::empty();
612 let blue_box = Rendered {
614 lines: vec!["\x1b[44m \x1b[0m".into()],
615 cursor: None,
616 images: Vec::new(),
617 };
618 blue_box.blit_into_rect(&mut target, Rect::new(0, 0, 8, 1));
619
620 let text = Rendered {
622 lines: vec!["hello".into()],
623 cursor: None,
624 images: Vec::new(),
625 };
626 text.blit_into_rect(&mut target, Rect::new(8, 0, 5, 1));
627
628 assert!(
630 target.lines[0].contains("\x1b[0mhello"),
631 "reset should be preserved before hello: {}",
632 target.lines[0]
633 );
634 assert_eq!(crate::utils::visible_width(&target.lines[0]), 13);
635 }
636
637 #[test]
639 fn blit_onto_ansi_target() {
640 let mut target = Rendered {
641 lines: vec!["\x1b[31mred text\x1b[0m".into()],
642 cursor: None,
643 images: Vec::new(),
644 };
645 let source = Rendered {
646 lines: vec!["XY".into()],
647 cursor: None,
648 images: Vec::new(),
649 };
650 source.blit_onto(&mut target, 0, 4);
652 assert!(target.lines[0].contains("XY"));
653 assert_eq!(crate::utils::visible_width(&target.lines[0]), 8);
654 }
655
656 #[test]
658 fn diff_resets_ansi_before_clear() {
659 let mut term = TestTerminal::new(80, 24);
660 let mut renderer = Renderer::new();
661
662 let frame1 = Rendered {
663 lines: vec!["\x1b[41mred bg\x1b[0m".into()],
664 cursor: None,
665 images: Vec::new(),
666 };
667 renderer.render(&mut term, &frame1).unwrap();
668
669 renderer.set_strategy(RenderStrategy::Diff);
670 let frame2 = Rendered {
671 lines: vec!["plain".into()],
672 cursor: None,
673 images: Vec::new(),
674 };
675 renderer.render(&mut term, &frame2).unwrap();
676
677 let written = term.written().join("");
678 for chunk in written.split("\x1b[2K") {
680 if !chunk.is_empty() && chunk.contains("\x1b[") {
681 assert!(
682 chunk.ends_with("\x1b[0m") || !chunk.contains("\x1b[2K"),
683 "clear must be preceded by reset: {}",
684 chunk
685 );
686 }
687 }
688 }
689
690 #[test]
692 fn first_render_resets_before_clear() {
693 let mut term = TestTerminal::new(80, 24);
694 let mut renderer = Renderer::new();
695 let rendered = Rendered {
696 lines: vec!["hello".into()],
697 cursor: None,
698 images: Vec::new(),
699 };
700 renderer.render(&mut term, &rendered).unwrap();
701 let written = term.written().join("");
702 assert!(
703 written.contains("\x1b[0m\x1b[2J"),
704 "reset must precede screen clear"
705 );
706 }
707
708 #[test]
710 fn full_redraw_resets_before_clear() {
711 let mut term = TestTerminal::new(80, 24);
712 let mut renderer = Renderer::new();
713 renderer.set_strategy(RenderStrategy::FullRedraw);
714 let rendered = Rendered {
715 lines: vec!["hello".into()],
716 cursor: None,
717 images: Vec::new(),
718 };
719 renderer.render(&mut term, &rendered).unwrap();
720 let written = term.written().join("");
721 assert!(
722 written.contains("\x1b[0m\x1b[2J"),
723 "reset must precede screen clear"
724 );
725 }
726}