1mod screen;
41
42use std::io::{self, Write};
43use std::sync::Arc;
44
45use rich::{Console, ConsoleOptions, Renderable, Segment};
46
47use crate::frame::{Change, Frame, Placement, PlacementSource, plan_graphics};
48
49pub(crate) use screen::char_width;
50pub use screen::{Cell, PaintStats, PaintStrategy, Painter, Rect, Screen, TextRun};
51
52pub trait Draw: Renderable {
86 fn draw(&self, console: &Console, options: &ConsoleOptions, screen: &mut Screen, rect: Rect);
90}
91
92pub fn draw_rendered(
95 console: &Console,
96 renderable: &dyn Renderable,
97 options: &ConsoleOptions,
98 screen: &mut Screen,
99 rect: Rect,
100) {
101 let lines = console.render_lines(renderable, options, false);
102 screen.write_lines(rect, &lines);
103}
104
105pub const BEGIN_SYNCHRONIZED_UPDATE: &str = "\x1b[?2026h";
108pub const END_SYNCHRONIZED_UPDATE: &str = "\x1b[?2026l";
111
112pub fn control_picture(c: char) -> Option<char> {
119 match c {
120 '\t' => Some(' '),
121 '\u{1b}' => Some('␛'),
122 '\u{7f}' => Some('␡'),
123 '\0'..='\u{1f}' => char::from_u32(0x2400 + c as u32),
124 '\u{80}'..='\u{9f}' => Some('\u{fffd}'),
125 _ => None,
126 }
127}
128
129pub fn sanitize_line(line: &mut [Segment]) {
136 for segment in line.iter_mut().filter(|segment| !segment.control) {
137 if segment.text.chars().any(|c| control_picture(c).is_some()) {
138 segment.text = segment
139 .text
140 .chars()
141 .map(|c| control_picture(c).unwrap_or(c))
142 .collect();
143 }
144 }
145}
146
147#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
149pub enum Viewport {
150 #[default]
153 Fullscreen,
154 Inline(u16),
159}
160
161pub struct Surface<W: Write> {
165 writer: W,
166 console: Console,
167 screen: Screen,
168 painter: Painter,
169 viewport: Viewport,
170 synchronized: bool,
171 started: bool,
172 finished: bool,
173 cursor_shown: bool,
174 full: bool,
176 graphics: Option<Arc<dyn PlacementSource>>,
177 placements: Vec<Placement>,
178 strategy: Option<PaintStrategy>,
180 terminal_scroll: Option<bool>,
182 under_tmux: bool,
184}
185
186impl Surface<io::Stdout> {
187 pub fn stdout(viewport: Viewport) -> Surface<io::Stdout> {
190 let console = Console::new();
191 let size = console.size();
192 let width = clamp(size.width);
193 let height = match viewport {
194 Viewport::Fullscreen => clamp(size.height),
195 Viewport::Inline(rows) => rows.min(clamp(size.height)),
197 };
198 Surface::new(io::stdout(), width, height)
199 .viewport(viewport)
200 .console(console)
201 }
202}
203
204impl<W: Write> Surface<W> {
205 pub fn new(writer: W, width: u16, height: u16) -> Surface<W> {
211 let console = Console::builder()
212 .width(width.max(1) as usize)
213 .height(height.max(1) as usize)
214 .force_terminal(true)
215 .color_system(Some(rich::ColorSystem::Truecolor))
216 .build();
217 let system = console.color_system();
218 Surface {
219 writer,
220 console,
221 screen: Screen::new(width, height),
222 painter: Painter::new(system),
223 viewport: Viewport::Fullscreen,
224 synchronized: false,
225 started: false,
226 finished: false,
227 cursor_shown: true,
228 full: true,
229 graphics: None,
230 placements: Vec::new(),
231 strategy: None,
232 terminal_scroll: None,
233 under_tmux: std::env::var_os("TMUX").is_some(),
234 }
235 .configured()
236 }
237
238 fn configured(mut self) -> Surface<W> {
243 self.painter.set_strategy(
244 self.strategy
245 .or_else(PaintStrategy::from_env)
246 .unwrap_or_default(),
247 );
248 let scroll = self
251 .terminal_scroll
252 .unwrap_or(!cfg!(windows) && !self.under_tmux);
253 self.painter.set_scroll(scroll);
254 self
255 }
256
257 #[must_use]
277 pub fn paint_strategy(mut self, strategy: PaintStrategy) -> Surface<W> {
278 self.strategy = Some(strategy);
279 self.configured()
280 }
281
282 #[must_use]
288 pub fn terminal_scroll(mut self, on: bool) -> Surface<W> {
289 self.terminal_scroll = Some(on);
290 self.configured()
291 }
292
293 #[must_use]
296 pub fn viewport(mut self, viewport: Viewport) -> Surface<W> {
297 self.viewport = viewport;
298 if let Viewport::Inline(rows) = viewport {
299 let area = self.screen.area();
302 self.screen = Screen::new(area.width, rows.min(area.height.max(1)));
303 }
304 let system = self.console.color_system();
305 self.painter = match viewport {
306 Viewport::Fullscreen => Painter::new(system),
307 Viewport::Inline(_) => Painter::new(system).inline(),
308 };
309 self.full = true;
310 self.configured()
311 }
312
313 #[must_use]
316 pub fn console(mut self, console: Console) -> Surface<W> {
317 self.painter.set_color_system(console.color_system());
318 self.console = console;
319 self
320 }
321
322 #[must_use]
326 pub fn synchronized(mut self, on: bool) -> Surface<W> {
327 self.synchronized = on;
328 self
329 }
330
331 #[must_use]
334 pub fn graphics(mut self, source: Option<Arc<dyn PlacementSource>>) -> Surface<W> {
335 self.graphics = source;
336 self
337 }
338
339 pub fn area(&self) -> Rect {
341 self.screen.area()
342 }
343
344 pub fn screen(&self) -> &Screen {
346 &self.screen
347 }
348
349 pub fn writer(&self) -> &W {
350 &self.writer
351 }
352
353 pub fn writer_mut(&mut self) -> &mut W {
354 &mut self.writer
355 }
356
357 pub fn resize(&mut self, width: u16, height: u16) {
362 let height = match self.viewport {
363 Viewport::Fullscreen => height,
364 Viewport::Inline(rows) => rows.min(height.max(1)),
365 };
366 if (width, height) == (self.screen.area().width, self.screen.area().height) {
367 return;
368 }
369 let mut screen = Screen::new(width, height);
370 let old = self.screen.area();
371 screen.blit(&self.screen, old, 0, 0);
372 self.screen = screen;
373 self.full = true;
374 }
375
376 pub fn clear(&mut self) {
378 let area = self.screen.area();
379 self.screen.clear(area);
380 self.full = true;
381 }
382
383 pub fn draw(&mut self, f: impl FnOnce(&mut Drawing<'_>)) -> io::Result<()> {
388 let mut drawing = Drawing {
389 screen: &mut self.screen,
390 console: &self.console,
391 graphics: self.graphics.as_deref(),
392 damage: Vec::new(),
393 cursor: None,
394 };
395 f(&mut drawing);
396 let Drawing { damage, cursor, .. } = drawing;
397 let out = self.frame(damage, cursor);
398 self.writer.write_all(out.as_bytes())?;
399 self.writer.flush()
400 }
401
402 fn frame(&mut self, damage: Vec<Rect>, cursor: Option<(u16, u16)>) -> String {
404 let mut out = String::new();
405 if !self.started {
406 if self.viewport == Viewport::Fullscreen {
407 out.push_str("\x1b[?1049h");
408 }
409 self.started = true;
410 }
411 let full = std::mem::take(&mut self.full);
412 if full {
413 self.painter.invalidate();
414 }
415 let damage = if full {
416 vec![self.screen.area()]
417 } else {
418 damage
419 };
420 let fixed: Vec<Rect> = self
423 .placements
424 .iter()
425 .map(|p| Rect::new(p.column as u16, p.row as u16, p.cols as u16, p.rows as u16))
426 .collect();
427 self.painter.set_fixed(&fixed);
428 let painted = self.painter.paint(&self.screen, &damage);
429 let written = self.painter.written().to_vec();
430 if self.cursor_shown && !painted.is_empty() {
431 out.push_str("\x1b[?25l");
434 self.cursor_shown = false;
435 }
436 out.push_str(&painted);
437 if self.graphics.is_some() {
438 out.push_str(&self.draw_graphics(&written));
439 }
440 let area = self.screen.area();
441 match cursor.filter(|&(x, y)| x < area.width && y < area.height) {
442 Some((x, y)) => {
443 out.push_str(&self.painter.move_to(x, y));
444 if !self.cursor_shown {
445 out.push_str("\x1b[?25h");
446 self.cursor_shown = true;
447 }
448 }
449 None => {
450 if self.cursor_shown {
451 out.push_str("\x1b[?25l");
452 self.cursor_shown = false;
453 }
454 }
455 }
456 self.painter.compact(&mut self.screen);
458 if self.synchronized && !out.is_empty() {
459 out = format!("{BEGIN_SYNCHRONIZED_UPDATE}{out}{END_SYNCHRONIZED_UPDATE}");
460 }
461 out
462 }
463
464 fn draw_graphics(&mut self, written: &[Rect]) -> String {
469 let Some(source) = self.graphics.clone() else {
470 return String::new();
471 };
472 let frame = screen_frame(&self.screen);
473 let height = frame.height();
474 let next: Vec<Placement> = source
475 .placements(&frame)
476 .into_iter()
477 .filter(|p| p.row + p.rows <= height)
478 .collect();
479 let changes: Vec<Change> = written
480 .iter()
481 .flat_map(|rect| {
482 (rect.y..rect.bottom()).map(move |row| Change {
483 row: row as usize,
484 columns: rect.x as usize..rect.right() as usize,
485 })
486 })
487 .collect();
488 let plan = plan_graphics(&self.placements, &next, &changes);
489 let mut out = String::new();
490 let repaint: Vec<Rect> = plan
491 .repaint
492 .iter()
493 .map(|(row, columns)| {
494 Rect::new(
495 columns.start as u16,
496 *row as u16,
497 (columns.end - columns.start) as u16,
498 1,
499 )
500 })
501 .collect();
502 if !repaint.is_empty() {
503 out.push_str(&self.painter.repaint(&self.screen, &repaint));
504 }
505 for placement in &plan.draw {
506 out.push_str(
507 &self
508 .painter
509 .move_to(placement.column as u16, placement.row as u16),
510 );
511 out.push_str(&placement.graphic.draw(placement.frame));
512 }
513 self.placements = next;
514 out
515 }
516
517 pub fn finish(&mut self) -> io::Result<()> {
521 if self.finished || !self.started {
522 self.finished = true;
523 return Ok(());
524 }
525 let mut out = String::new();
526 if let Some(source) = &self.graphics {
527 let retained = match self.viewport {
528 Viewport::Inline(_) => self.placements.clone(),
529 Viewport::Fullscreen => Vec::new(),
530 };
531 out.push_str(&source.release(&retained));
532 }
533 out.push_str(&self.painter.finish());
534 out.push_str("\x1b[0m\x1b[?25h");
535 if self.viewport == Viewport::Fullscreen {
536 out.push_str("\x1b[?1049l");
537 }
538 self.writer.write_all(out.as_bytes())?;
539 self.writer.flush()?;
540 self.finished = true;
542 Ok(())
543 }
544}
545
546impl<W: Write> Drop for Surface<W> {
547 fn drop(&mut self) {
548 let _ = self.finish();
549 }
550}
551
552pub struct Drawing<'a> {
555 screen: &'a mut Screen,
556 console: &'a Console,
557 graphics: Option<&'a dyn PlacementSource>,
558 damage: Vec<Rect>,
559 cursor: Option<(u16, u16)>,
560}
561
562impl Drawing<'_> {
563 pub fn area(&self) -> Rect {
565 self.screen.area()
566 }
567
568 pub fn console(&self) -> &Console {
570 self.console
571 }
572
573 pub fn render(&mut self, rect: Rect, renderable: &dyn Renderable) {
577 let options = self
578 .console
579 .options()
580 .update_width(rect.width.max(1) as usize)
581 .update_height(rect.height.max(1) as usize);
582 let lines = self.console.render_lines(renderable, &options, false);
583 self.write_lines(rect, lines);
584 }
585
586 pub fn draw(&mut self, rect: Rect, drawable: &dyn Draw) {
590 if self.graphics.is_some() {
591 return self.render(rect, drawable);
592 }
593 let options = self
594 .console
595 .options()
596 .update_width(rect.width.max(1) as usize)
597 .update_height(rect.height.max(1) as usize);
598 drawable.draw(self.console, &options, self.screen, rect);
599 self.damage.push(rect);
600 }
601
602 pub fn write_lines(&mut self, rect: Rect, lines: Vec<Vec<Segment>>) {
605 let lines: Vec<Vec<Segment>> = match self.graphics {
606 Some(source) => lines.into_iter().map(|l| source.prepare(l)).collect(),
607 None => lines,
608 };
609 self.screen.write_lines(rect, &lines);
610 self.damage.push(rect);
611 }
612
613 pub fn clear(&mut self, rect: Rect) {
615 self.screen.clear(rect);
616 self.damage.push(rect);
617 }
618
619 pub fn set_cursor(&mut self, x: u16, y: u16) {
621 self.cursor = Some((x, y));
622 }
623
624 pub fn screen_mut(&mut self) -> &mut Screen {
628 let area = self.screen.area();
629 self.damage.push(area);
630 self.screen
631 }
632}
633
634fn screen_frame(screen: &Screen) -> Frame {
636 let mut segments = Vec::new();
637 for line in screen.lines() {
638 segments.extend(line);
639 segments.push(Segment::line());
640 }
641 Frame::from_segments(&segments)
642}
643
644fn clamp(cells: usize) -> u16 {
645 cells.clamp(1, u16::MAX as usize) as u16
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use rich::Text;
652
653 fn output(surface: &mut Surface<Vec<u8>>) -> String {
654 String::from_utf8(std::mem::take(surface.writer_mut())).unwrap()
655 }
656
657 fn painting(strategy: PaintStrategy, tmux: bool, width: u16, height: u16) -> Surface<Vec<u8>> {
660 let mut surface = Surface::new(Vec::new(), width, height);
661 surface.under_tmux = tmux;
662 surface.paint_strategy(strategy)
663 }
664
665 fn scrolls(out: &str) -> bool {
668 let bytes = out.as_bytes();
669 bytes.iter().enumerate().any(|(i, &b)| {
670 if b != 0x1b || bytes.get(i + 1) != Some(&b'[') {
671 return false;
672 }
673 let rest = &bytes[i + 2..];
674 let digits = rest
675 .iter()
676 .take_while(|c| c.is_ascii_digit() || **c == b';')
677 .count();
678 matches!(rest.get(digits), Some(b'S' | b'T' | b'r'))
679 })
680 }
681
682 fn differences(a: &vt100::Screen, b: &vt100::Screen, width: u16, height: u16) -> Vec<String> {
685 let mut found = Vec::new();
686 for row in 0..height {
687 for col in 0..width {
688 let (Some(x), Some(y)) = (a.cell(row, col), b.cell(row, col)) else {
689 continue;
690 };
691 let look = |c: &vt100::Cell| {
692 (
693 match c.contents() {
695 "" => " ".to_string(),
696 text => text.to_string(),
697 },
698 c.fgcolor(),
699 c.bgcolor(),
700 c.bold(),
701 c.italic(),
702 c.underline(),
703 c.inverse(),
704 )
705 };
706 if look(x) != look(y) {
707 found.push(format!("({col}, {row}): {:?} != {:?}", look(x), look(y)));
708 }
709 }
710 }
711 found
712 }
713
714 fn best_matches_full(frames: &[Vec<String>], width: u16, height: u16, tmux: bool) -> String {
718 let mut best = painting(PaintStrategy::Best, tmux, width, height);
719 let mut full = painting(PaintStrategy::Full, tmux, width, height);
720 let mut best_term = vt100::Parser::new(height, width, 0);
721 let mut full_term = vt100::Parser::new(height, width, 0);
722 let mut later = String::new();
723 for (n, lines) in frames.iter().enumerate() {
724 for (is_best, surface, term) in [
725 (true, &mut best, &mut best_term),
726 (false, &mut full, &mut full_term),
727 ] {
728 surface
729 .draw(|s| {
730 for (y, line) in lines.iter().enumerate() {
731 let text = Text::from_markup(line).unwrap();
732 s.render(Rect::new(0, y as u16, width, 1), &text);
733 }
734 })
735 .unwrap();
736 term.process(surface.writer());
737 if n > 0 && is_best {
738 later.push_str(std::str::from_utf8(surface.writer()).unwrap());
739 }
740 surface.writer_mut().clear();
741 }
742 let wrong = differences(best_term.screen(), full_term.screen(), width, height);
743 assert!(wrong.is_empty(), "frame {n}: {wrong:?}");
744 }
745 later
746 }
747
748 fn scrolling_log(frames: usize, rows: usize) -> Vec<Vec<String>> {
751 const STYLES: [&str; 4] = ["red", "bold green", "italic", "on blue"];
752 (0..frames)
753 .map(|frame| {
754 (frame..frame + rows)
755 .map(|i| format!("[{}]entry {i}[/] of the log", STYLES[i % STYLES.len()]))
756 .collect()
757 })
758 .collect()
759 }
760
761 #[test]
762 fn surfaces_paint_with_best_by_default() {
763 if PaintStrategy::from_env().is_none() {
764 let surface = Surface::new(Vec::new(), 4, 1);
765 assert_eq!(surface.painter.strategy(), PaintStrategy::Best);
766 }
767 let surface = Surface::new(Vec::new(), 4, 1).paint_strategy(PaintStrategy::Rows);
768 assert_eq!(surface.painter.strategy(), PaintStrategy::Rows);
769 let surface = surface.viewport(Viewport::Inline(1));
771 assert_eq!(surface.painter.strategy(), PaintStrategy::Rows);
772 }
773
774 #[test]
775 fn best_replayed_equals_a_full_repaint_when_rows_scroll() {
776 let later = best_matches_full(&scrolling_log(8, 6), 30, 6, false);
777 if cfg!(windows) {
778 assert!(!scrolls(&later), "{later:?}");
779 } else {
780 assert!(scrolls(&later), "best should have scrolled: {later:?}");
781 }
782 }
783
784 #[test]
785 fn best_replayed_equals_a_full_repaint_when_nothing_scrolls() {
786 let frames: Vec<Vec<String>> = (0..6)
787 .map(|t| {
788 vec![
789 "[bold]a clock[/]".to_string(),
790 format!("tick [cyan]{t}[/] of [red]{}[/]", 10 - t),
791 "#".repeat(t + 1),
792 ]
793 })
794 .collect();
795 let later = best_matches_full(&frames, 20, 3, false);
796 assert!(!scrolls(&later), "{later:?}");
797 }
798
799 #[test]
800 fn under_tmux_best_does_not_scroll() {
801 let later = best_matches_full(&scrolling_log(8, 6), 30, 6, true);
802 assert!(!scrolls(&later), "{later:?}");
803 let mut surface =
805 painting(PaintStrategy::Best, true, 30, 6).terminal_scroll(!cfg!(windows));
806 let mut out = String::new();
807 for (n, lines) in scrolling_log(4, 6).iter().enumerate() {
808 surface
809 .draw(|s| {
810 for (y, line) in lines.iter().enumerate() {
811 let text = Text::from_markup(line).unwrap();
812 s.render(Rect::new(0, y as u16, 30, 1), &text);
813 }
814 })
815 .unwrap();
816 let frame = output(&mut surface);
817 if n > 0 {
818 out.push_str(&frame);
819 }
820 }
821 assert_eq!(scrolls(&out), !cfg!(windows), "{out:?}");
822 }
823
824 #[test]
825 fn an_inline_surface_does_not_scroll() {
826 let mut surface = painting(PaintStrategy::Best, false, 30, 6)
827 .viewport(Viewport::Inline(6))
828 .terminal_scroll(true);
829 let mut out = String::new();
830 for (n, lines) in scrolling_log(4, 6).iter().enumerate() {
831 surface
832 .draw(|s| {
833 for (y, line) in lines.iter().enumerate() {
834 let text = Text::from_markup(line).unwrap();
835 s.render(Rect::new(0, y as u16, 30, 1), &text);
836 }
837 })
838 .unwrap();
839 let frame = output(&mut surface);
840 if n > 0 {
841 out.push_str(&frame);
842 }
843 }
844 assert!(!scrolls(&out), "{out:?}");
845 }
846
847 #[test]
848 fn the_first_draw_enters_the_alternate_screen_and_writes_everything() {
849 let mut surface = Surface::new(Vec::new(), 10, 2);
850 surface
851 .draw(|s| s.render(Rect::new(0, 0, 10, 1), &Text::new("hello")))
852 .unwrap();
853 let out = output(&mut surface);
854 assert!(out.starts_with("\x1b[?1049h\x1b[?25l"), "{out:?}");
855 assert!(out.contains("\x1b[2J") && out.contains("hello"), "{out:?}");
856 surface.finish().unwrap();
857 let out = output(&mut surface);
858 assert!(out.ends_with("\x1b[0m\x1b[?25h\x1b[?1049l"), "{out:?}");
859 }
860
861 #[test]
862 fn an_unchanged_frame_writes_nothing() {
863 let mut surface = Surface::new(Vec::new(), 10, 2);
864 let draw = |s: &mut Drawing<'_>| s.render(Rect::new(0, 0, 10, 1), &Text::new("same"));
865 surface.draw(draw).unwrap();
866 output(&mut surface);
867 surface.draw(draw).unwrap();
868 assert_eq!(output(&mut surface), "");
869 }
870
871 #[test]
872 fn a_resize_keeps_the_cells_and_writes_everything_once() {
873 let mut surface = Surface::new(Vec::new(), 10, 2);
874 surface
875 .draw(|s| s.render(Rect::new(0, 0, 10, 1), &Text::new("kept")))
876 .unwrap();
877 output(&mut surface);
878 surface.resize(6, 3);
879 assert_eq!(surface.screen().plain()[0], "kept ");
880 surface.draw(|_| {}).unwrap();
881 let out = output(&mut surface);
882 assert!(out.contains("\x1b[2J") && out.contains("kept"), "{out:?}");
883 surface.draw(|_| {}).unwrap();
884 assert_eq!(output(&mut surface), "");
885 }
886
887 #[test]
888 fn inline_draws_below_the_cursor_and_leaves_the_frame_on_finish() {
889 let mut surface = Surface::new(Vec::new(), 8, 24).viewport(Viewport::Inline(2));
890 assert_eq!(surface.area(), Rect::new(0, 0, 8, 2));
891 surface
892 .draw(|s| s.render(s.area(), &Text::new("one\ntwo")))
893 .unwrap();
894 let out = output(&mut surface);
895 assert!(!out.contains("\x1b[?1049h"), "{out:?}");
896 assert!(!out.contains("\x1b[2J"), "{out:?}");
897 assert!(out.contains("one") && out.contains("two"), "{out:?}");
898 surface.finish().unwrap();
899 let out = output(&mut surface);
900 assert!(out.ends_with("\r\n\x1b[0m\x1b[?25h"), "{out:?}");
901 }
902
903 #[test]
904 fn the_cursor_shows_only_where_a_frame_puts_it() {
905 let mut surface = Surface::new(Vec::new(), 10, 2);
906 surface
907 .draw(|s| {
908 s.render(Rect::new(0, 0, 10, 1), &Text::new("> "));
909 s.set_cursor(2, 0);
910 })
911 .unwrap();
912 let out = output(&mut surface);
913 assert!(out.ends_with("\x1b[1;3H\x1b[?25h"), "{out:?}");
914 surface.draw(|_| {}).unwrap();
915 assert_eq!(output(&mut surface), "\x1b[?25l");
916 }
917
918 #[test]
919 fn a_synchronized_surface_wraps_each_frame() {
920 let mut surface = Surface::new(Vec::new(), 10, 1).synchronized(true);
921 surface
922 .draw(|s| s.render(s.area(), &Text::new("x")))
923 .unwrap();
924 let out = output(&mut surface);
925 assert!(out.starts_with(BEGIN_SYNCHRONIZED_UPDATE), "{out:?}");
926 assert!(out.ends_with(END_SYNCHRONIZED_UPDATE), "{out:?}");
927 surface.draw(|_| {}).unwrap();
928 assert_eq!(output(&mut surface), "", "nothing to write, no wrapper");
929 }
930
931 #[test]
932 fn controls_in_rendered_text_become_visible() {
933 let mut surface = Surface::new(Vec::new(), 10, 1);
934 surface
935 .draw(|s| s.render(s.area(), &Text::new("a\x1b[2Jb")))
936 .unwrap();
937 assert_eq!(surface.screen().plain()[0], "a␛[2Jb ");
938 }
939
940 struct Flaky {
942 fails: usize,
943 out: Vec<u8>,
944 }
945
946 impl Write for Flaky {
947 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
948 if self.fails > 0 {
949 self.fails -= 1;
950 return Err(io::Error::other("full"));
951 }
952 self.out.write(buf)
953 }
954 fn flush(&mut self) -> io::Result<()> {
955 Ok(())
956 }
957 }
958
959 #[test]
960 fn a_failed_finish_can_be_retried() {
961 let mut surface = Surface::new(
962 Flaky {
963 fails: 0,
964 out: Vec::new(),
965 },
966 4,
967 1,
968 );
969 surface
970 .draw(|s| s.render(s.area(), &Text::new("x")))
971 .unwrap();
972 surface.writer_mut().fails = 1;
973 assert!(surface.finish().is_err());
974 surface.finish().unwrap();
975 let out = String::from_utf8(surface.writer().out.clone()).unwrap();
976 assert!(out.ends_with("\x1b[?1049l"), "{out:?}");
977 }
978
979 #[derive(Debug)]
981 struct Star;
982
983 impl crate::frame::Graphic for Star {
984 fn key(&self) -> u64 {
985 1
986 }
987 fn draw(&self, _frame: usize) -> String {
988 "<star>".into()
989 }
990 }
991
992 struct Moving(std::sync::Mutex<usize>);
994
995 impl PlacementSource for Moving {
996 fn placements(&self, _frame: &Frame) -> Vec<Placement> {
997 vec![Placement {
998 row: 0,
999 column: *self.0.lock().unwrap(),
1000 cols: 1,
1001 rows: 1,
1002 graphic: Arc::new(Star),
1003 frame: 0,
1004 }]
1005 }
1006 }
1007
1008 #[test]
1009 fn a_graphic_that_moves_off_unchanged_cells_leaves_no_ghost() {
1010 let source = Arc::new(Moving(std::sync::Mutex::new(1)));
1011 let mut surface = Surface::new(Vec::new(), 6, 1)
1012 .graphics(Some(source.clone() as Arc<dyn PlacementSource>));
1013 let draw = |s: &mut Drawing<'_>| s.render(s.area(), &Text::new("abcdef"));
1014 surface.draw(draw).unwrap();
1015 output(&mut surface);
1016 *source.0.lock().unwrap() = 3;
1017 surface.draw(draw).unwrap();
1020 let out = output(&mut surface);
1021 assert_eq!(out, "\x1b[1;2H\x1b[0mb\x1b[1;4H<star>", "{out:?}");
1022 }
1023
1024 #[test]
1025 fn dropping_a_started_surface_gives_the_terminal_back() {
1026 let mut sink = Vec::new();
1027 {
1028 let mut surface = Surface::new(&mut sink, 4, 1);
1029 surface
1030 .draw(|s| s.render(s.area(), &Text::new("x")))
1031 .unwrap();
1032 }
1033 let out = String::from_utf8(sink).unwrap();
1034 assert!(out.ends_with("\x1b[?1049l"), "{out:?}");
1035 }
1036}