1use std::{collections::BTreeSet, mem, panic};
2
3use device::{ScreenDevice, ScreenDeviceExt};
4use thedes_async_util::{
5 non_blocking,
6 timer::{TickSession, Timer},
7};
8use thedes_geometry::rect;
9use thiserror::Error;
10use tokio::task;
11use tokio_util::sync::CancellationToken;
12
13use crate::{
14 color::{BasicColor, Color, ColorPair},
15 geometry::{Coord, CoordPair},
16 grapheme,
17 input::TermSizeWatch,
18 mutation::{BoxedMutation, Mutation},
19 runtime,
20 status::Status,
21 tile::{Tile, TileMutationError},
22};
23
24pub mod device;
25
26#[derive(Debug, Error)]
27#[error("Point is outside of screen canvas rectangle")]
28pub struct InvalidCanvasPoint {
29 #[from]
30 source: rect::HorzAreaError<usize>,
31}
32
33#[derive(Debug, Error)]
34#[error("Index is outside of screen canvas buffer bounds")]
35pub struct InvalidCanvasIndex {
36 #[from]
37 source: rect::InvalidArea<usize>,
38}
39
40#[derive(Debug, Error)]
41pub enum Error {
42 #[error("Failed to control screen device")]
43 Device(
44 #[from]
45 #[source]
46 device::Error,
47 ),
48 #[error("Invalid terminal point while drawing canvas, point {0}")]
49 InvalidTermPoint(CoordPair),
50 #[error("Invalid canvas point while drawing canvas")]
51 InvalidCanvasPoint(
52 #[from]
53 #[source]
54 InvalidCanvasPoint,
55 ),
56 #[error("Invalid canvas index while drawing canvas")]
57 InvalidCanvasIndex(
58 #[from]
59 #[source]
60 InvalidCanvasIndex,
61 ),
62 #[error("Found unknown grapheme ID")]
63 UnknownGrapheme(
64 #[from]
65 #[source]
66 grapheme::UnknownId,
67 ),
68 #[error("Failed to register grapheme")]
69 NotGrapheme(
70 #[from]
71 #[source]
72 grapheme::NotGrapheme,
73 ),
74 #[error("Failed to mutate tile contents in canvas point {0}")]
75 TileMutation(CoordPair, #[source] TileMutationError),
76}
77
78#[derive(Debug, Error)]
79#[error(transparent)]
80pub struct FlushError {
81 inner: non_blocking::spsc::unbounded::SendError<Vec<Command>>,
82}
83
84impl FlushError {
85 fn new(
86 inner: non_blocking::spsc::unbounded::SendError<Vec<Command>>,
87 ) -> Self {
88 Self { inner }
89 }
90
91 pub fn into_bounced_commands(self) -> Vec<Command> {
92 self.inner.into_message()
93 }
94}
95
96#[derive(Debug)]
97pub enum Command {
98 Mutation(CoordPair, Box<dyn BoxedMutation<Tile>>),
99 ClearScreen(Color),
100}
101
102impl Command {
103 pub fn new_mutation<M>(canvas_point: CoordPair, mutation: M) -> Self
104 where
105 M: Mutation<Tile>,
106 {
107 Self::Mutation(canvas_point, Box::new(mutation))
108 }
109
110 pub fn new_clear_screen<C>(color: C) -> Self
111 where
112 C: Into<Color>,
113 {
114 Self::ClearScreen(color.into())
115 }
116}
117
118#[derive(Debug)]
119pub(crate) struct OpenResources {
120 pub device: Box<dyn ScreenDevice>,
121 pub timer: Timer,
122 pub cancel_token: CancellationToken,
123 pub grapheme_registry: grapheme::Registry,
124 pub term_size_watch: TermSizeWatch,
125 pub status: Status,
126}
127
128#[derive(Debug, Clone)]
129pub struct Config {
130 canvas_size: CoordPair,
131 default_colors: ColorPair,
132}
133
134impl Config {
135 pub fn new() -> Self {
136 Self {
137 canvas_size: CoordPair { y: 22, x: 78 },
138 default_colors: ColorPair {
139 background: BasicColor::Black.into(),
140 foreground: BasicColor::White.into(),
141 },
142 }
143 }
144
145 pub fn with_canvas_size(self, size: CoordPair) -> Self {
146 Self { canvas_size: size, ..self }
147 }
148
149 pub fn with_default_color(self, color: ColorPair) -> Self {
150 Self { default_colors: color, ..self }
151 }
152
153 pub fn canvas_size(&self) -> CoordPair {
154 self.canvas_size
155 }
156
157 pub(crate) fn open(
158 self,
159 mut resources: OpenResources,
160 join_set: &mut runtime::JoinSet,
161 ) -> ScreenHandles {
162 let canvas_size = self.canvas_size;
163 let (command_sender, command_receiver) =
164 non_blocking::spsc::unbounded::channel();
165 let status = resources.status.clone();
166
167 join_set.spawn(async move {
168 let initial_term_size = task::block_in_place(|| {
169 resources.device.blocking_get_size().map_err(Error::from)
170 })?;
171 let renderer = Renderer::new(
172 self,
173 resources,
174 initial_term_size,
175 command_receiver,
176 );
177 renderer.run().await
178 });
179
180 let canvas_handle =
181 CanvasHandle::new(canvas_size, status, command_sender);
182 ScreenHandles { canvas: canvas_handle }
183 }
184}
185
186#[derive(Debug)]
187pub(crate) struct ScreenHandles {
188 pub canvas: CanvasHandle,
189}
190
191#[derive(Debug)]
192struct Renderer {
193 device: Box<dyn ScreenDevice>,
194 device_queue: Vec<device::Command>,
195 ticker: TickSession,
196 cancel_token: CancellationToken,
197 term_size: CoordPair,
198 canvas_size: CoordPair,
199 default_colors: ColorPair,
200 current_colors: ColorPair,
201 current_position: CoordPair,
202 working_buf: Box<[Tile]>,
203 displayed_buf: Box<[Tile]>,
204 dirty: BTreeSet<CoordPair>,
205 grapheme_registry: grapheme::Registry,
206 command_receiver: non_blocking::spsc::unbounded::Receiver<Vec<Command>>,
207 term_size_watch: TermSizeWatch,
208 status: Status,
209}
210
211impl Renderer {
212 pub fn new(
213 config: Config,
214 resources: OpenResources,
215 term_size: CoordPair,
216 command_receiver: non_blocking::spsc::unbounded::Receiver<Vec<Command>>,
217 ) -> Self {
218 let tile_buf_size = usize::from(config.canvas_size.x)
219 * usize::from(config.canvas_size.y);
220 let tile_buf = Box::<[Tile]>::from(vec![
221 Tile {
222 grapheme: grapheme::Id::from(' '),
223 colors: config.default_colors,
224 };
225 tile_buf_size
226 ]);
227
228 Self {
229 device: resources.device,
230 device_queue: Vec::new(),
231 ticker: resources.timer.new_session(),
232 cancel_token: resources.cancel_token,
233 term_size,
234 canvas_size: config.canvas_size,
235 default_colors: config.default_colors,
236 current_colors: config.default_colors,
237 current_position: CoordPair { x: 0, y: 0 },
238 working_buf: tile_buf.clone(),
239 displayed_buf: tile_buf,
240 dirty: BTreeSet::new(),
241 grapheme_registry: resources.grapheme_registry,
242 command_receiver,
243 term_size_watch: resources.term_size_watch,
244 status: resources.status,
245 }
246 }
247
248 pub async fn run(mut self) -> Result<(), runtime::Error> {
249 let run_result = self.do_run().await;
250 self.shutdown().await.expect("Screen shutdown failed");
251 run_result
252 }
253
254 async fn do_run(&mut self) -> Result<(), runtime::Error> {
255 self.init().await?;
256
257 let mut commands = Vec::<Command>::new();
258
259 loop {
260 if !self.check_term_size_change().await? {
261 break;
262 }
263 self.render().await?;
264
265 tokio::select! {
266 _ = self.ticker.tick() => (),
267 _ = self.cancel_token.cancelled() => {
268 tracing::info!("Screen token cancellation detected");
269 break
270 },
271 }
272
273 if !self.check_term_size_change().await? {
274 break;
275 }
276 if !self.execute_commands_sent(&mut commands)? {
277 break;
278 }
279
280 tokio::select! {
281 _ = self.ticker.tick() => (),
282 _ = self.cancel_token.cancelled() => {
283 tracing::info!("Screen token cancellation detected");
284 break
285 },
286 }
287 }
288
289 Ok(())
290 }
291
292 fn queue(&mut self, commands: impl IntoIterator<Item = device::Command>) {
293 self.device_queue.extend(commands);
294 }
295
296 async fn flush(&mut self) -> Result<(), Error> {
297 let _ = self.device.send(self.device_queue.drain(..));
298 self.device.flush().await?;
299 Ok(())
300 }
301
302 async fn init(&mut self) -> Result<(), Error> {
303 self.enter()?;
304 let term_size = self.term_size;
305 self.status.set_blocked_from_sizes(self.canvas_size, self.term_size);
306 self.term_size_changed(term_size).await?;
307 Ok(())
308 }
309
310 async fn shutdown(&mut self) -> Result<(), Error> {
311 self.leave();
312 self.flush().await?;
313 Ok(())
314 }
315
316 async fn render(&mut self) -> Result<(), Error> {
317 if !self.needs_resize() {
318 self.draw_working_canvas()?;
319 self.flush().await?;
320 }
321 Ok(())
322 }
323
324 pub fn needs_resize(&self) -> bool {
325 self.status.is_blocked()
326 }
327
328 fn draw_working_canvas(&mut self) -> Result<(), Error> {
329 for canvas_point in mem::take(&mut self.dirty) {
330 let tile = self.get(canvas_point)?;
331 let term_point = self.canvas_to_term(canvas_point);
332 self.draw_tile(term_point, tile)?;
333 }
334 self.displayed_buf.clone_from(&self.working_buf);
335 Ok(())
336 }
337
338 fn enter(&mut self) -> Result<(), Error> {
339 self.queue([device::Command::Enter, device::Command::HideCursor]);
340 Ok(())
341 }
342
343 fn leave(&mut self) {
344 self.queue([
345 device::Command::ShowCursor,
346 device::Command::ResetBackground,
347 device::Command::ResetForeground,
348 device::Command::Clear,
349 device::Command::Leave,
350 ]);
351 }
352
353 async fn check_term_size_change(&mut self) -> Result<bool, Error> {
354 let Ok(term_size_message) = self.term_size_watch.recv() else {
355 tracing::info!("Terminal size watch sender disconnected");
356 return Ok(false);
357 };
358 if let Some(new_term_size) = term_size_message {
359 self.term_size_changed(new_term_size).await?;
360 }
361 Ok(true)
362 }
363
364 async fn term_size_changed(
365 &mut self,
366 new_term_size: CoordPair,
367 ) -> Result<(), Error> {
368 self.status.set_blocked_from_sizes(self.canvas_size, new_term_size);
369
370 let position = self.current_position;
371 self.move_to(position)?;
372 let colors = self.current_colors;
373 self.change_colors(colors)?;
374 self.dirty.clear();
375 let space = grapheme::Id::from(' ');
376
377 for (i, (working, displayed)) in self
378 .working_buf
379 .iter_mut()
380 .zip(&mut self.displayed_buf[..])
381 .enumerate()
382 {
383 *displayed = Tile { grapheme: space, colors: self.default_colors };
384 if *displayed != *working {
385 let point = self
386 .canvas_size
387 .map(usize::from)
388 .as_rect_size(thedes_geometry::CoordPair::from_axes(|_| 0))
389 .checked_bot_right_of_horz_area(&i)
390 .map_err(InvalidCanvasIndex::from)?
391 .map(|a| a as Coord);
392 self.dirty.insert(point);
393 }
394 }
395
396 self.term_size = new_term_size;
397 if self.needs_resize() {
398 self.draw_resize_msg()?;
399 } else {
400 self.draw_reset()?;
401 }
402
403 self.flush().await?;
404
405 Ok(())
406 }
407
408 fn move_to(&mut self, term_point: CoordPair) -> Result<(), Error> {
409 if term_point.zip2(self.term_size).any(|(point, size)| point >= size) {
410 Err(Error::InvalidTermPoint(term_point))?
411 }
412 self.queue([device::Command::MoveCursor(term_point)]);
413 self.current_position = term_point;
414 Ok(())
415 }
416
417 fn change_colors(&mut self, colors: ColorPair) -> Result<(), Error> {
418 self.change_foreground(colors.foreground)?;
419 self.change_background(colors.background)?;
420 Ok(())
421 }
422
423 fn change_foreground(&mut self, color: Color) -> Result<(), Error> {
424 self.queue([device::Command::SetForeground(color)]);
425 self.current_colors.foreground = color;
426 Ok(())
427 }
428
429 fn change_background(&mut self, color: Color) -> Result<(), Error> {
430 self.queue([device::Command::SetBackground(color)]);
431 self.current_colors.background = color;
432 Ok(())
433 }
434
435 fn clear_term(&mut self, background: Color) -> Result<(), Error> {
436 if background != self.current_colors.background {
437 self.change_background(background)?;
438 }
439 self.queue([device::Command::Clear]);
440 Ok(())
441 }
442
443 fn draw_reset_hor_line(
444 &mut self,
445 y: Coord,
446 x_start: Coord,
447 x_end: Coord,
448 ) -> Result<(), Error> {
449 let tile = Tile {
450 colors: self.default_colors,
451 grapheme: self.grapheme_registry.get_or_register("━")?,
452 };
453 for x in x_start .. x_end {
454 self.draw_tile(CoordPair { x, y }, tile)?;
455 }
456 Ok(())
457 }
458
459 fn draw_reset(&mut self) -> Result<(), Error> {
460 self.move_to(CoordPair { y: 0, x: 0 })?;
461 self.clear_term(self.default_colors.background)?;
462
463 let margin_top_left = self.top_left_margin();
464 let margin_bottom_right = self.bottom_right_margin();
465
466 let tile = Tile {
467 grapheme: self.grapheme_registry.get_or_register("┏")?,
468 colors: self.default_colors,
469 };
470 self.draw_tile(margin_top_left - 1, tile)?;
471 self.draw_reset_hor_line(
472 margin_top_left.y - 1,
473 margin_top_left.x,
474 margin_bottom_right.x,
475 )?;
476 let tile = Tile {
477 grapheme: self.grapheme_registry.get_or_register("┓")?,
478 colors: self.default_colors,
479 };
480 self.draw_tile(
481 CoordPair { x: margin_bottom_right.x, y: margin_top_left.y - 1 },
482 tile,
483 )?;
484
485 let tile = Tile {
486 grapheme: self.grapheme_registry.get_or_register("┃")?,
487 colors: self.default_colors,
488 };
489 for y in margin_top_left.y .. margin_bottom_right.y {
490 self.draw_tile(CoordPair { x: margin_top_left.x - 1, y }, tile)?;
491 self.draw_tile(CoordPair { x: margin_bottom_right.x, y }, tile)?;
492 }
493
494 let tile = Tile {
495 grapheme: self.grapheme_registry.get_or_register("┗")?,
496 colors: self.default_colors,
497 };
498 self.draw_tile(
499 CoordPair { x: margin_top_left.x - 1, y: margin_bottom_right.y },
500 tile,
501 )?;
502 self.draw_reset_hor_line(
503 margin_bottom_right.y,
504 margin_top_left.x,
505 margin_bottom_right.x,
506 )?;
507 let tile = Tile {
508 grapheme: self.grapheme_registry.get_or_register("┛")?,
509 colors: self.default_colors,
510 };
511 self.draw_tile(margin_bottom_right, tile)?;
512
513 Ok(())
514 }
515
516 fn draw_resize_msg(&mut self) -> Result<(), Error> {
517 let graphemes: Vec<_> = self
518 .grapheme_registry
519 .get_or_register_many(&format!(
520 "RESIZE {}x{}",
521 self.canvas_size.x + 2,
522 self.canvas_size.y + 2
523 ))
524 .collect();
525 self.move_to(CoordPair { y: 0, x: 0 })?;
526 self.clear_term(self.default_colors.background)?;
527 for (grapheme, i) in graphemes.into_iter().zip(0 ..) {
528 self.draw_tile(
529 CoordPair { x: i, y: 0 },
530 Tile { colors: self.default_colors, grapheme },
531 )?;
532 }
533 Ok(())
534 }
535
536 fn draw_tile(
537 &mut self,
538 term_point: CoordPair,
539 tile: Tile,
540 ) -> Result<(), Error> {
541 if self.current_position != term_point {
542 self.move_to(term_point)?;
543 }
544 if self.current_colors.foreground != tile.colors.foreground {
545 self.change_foreground(tile.colors.foreground)?;
546 }
547 if self.current_colors.background != tile.colors.background {
548 self.change_background(tile.colors.background)?;
549 }
550 self.draw_grapheme(tile.grapheme)?;
551 Ok(())
552 }
553
554 fn draw_grapheme(&mut self, id: grapheme::Id) -> Result<(), Error> {
555 self.grapheme_registry.lookup(id, |result| {
556 result.map(|chars| {
557 self.device_queue.extend(chars.map(device::Command::Write))
558 })
559 })?;
560 self.current_position.x += 1;
561 if self.current_position.x >= self.term_size.x {
562 self.current_position.x = 0;
563 self.current_position.y += 1;
564 if self.current_position.y >= self.term_size.y {
565 self.move_to(CoordPair { y: 0, x: 0 })?;
566 }
567 }
568 Ok(())
569 }
570
571 fn top_left_margin(&self) -> CoordPair {
572 (self.term_size - self.canvas_size + 1) / 2
573 }
574
575 fn bottom_right_margin(&self) -> CoordPair {
576 self.top_left_margin() + self.canvas_size
577 }
578
579 fn get(&self, canvas_point: CoordPair) -> Result<Tile, InvalidCanvasPoint> {
580 let index = self.point_to_index(canvas_point)?;
581 Ok(self.working_buf[index])
582 }
583
584 fn set(
585 &mut self,
586 canvas_point: CoordPair,
587 tile: Tile,
588 ) -> Result<Tile, InvalidCanvasPoint> {
589 let index = self.point_to_index(canvas_point)?;
590 if tile == self.displayed_buf[index] {
591 self.dirty.remove(&canvas_point);
592 } else {
593 self.dirty.insert(canvas_point);
594 }
595 let old = mem::replace(&mut self.working_buf[index], tile);
596 Ok(old)
597 }
598
599 fn execute_commands_sent(
600 &mut self,
601 buf: &mut Vec<Command>,
602 ) -> Result<bool, Error> {
603 let Ok(command_iterator) = self.command_receiver.recv_many() else {
604 tracing::info!("Screen command sender disconnected");
605 return Ok(false);
606 };
607 buf.extend(command_iterator.flatten());
608 for command in buf.drain(..) {
609 self.execute_command(command)?;
610 }
611 Ok(true)
612 }
613
614 fn execute_command(&mut self, command: Command) -> Result<(), Error> {
615 match command {
616 Command::Mutation(canvas_point, mutation) => {
617 self.execute_mutation(canvas_point, mutation)
618 },
619 Command::ClearScreen(color) => self.execute_clear_screen(color),
620 }
621 }
622
623 fn execute_mutation(
624 &mut self,
625 canvas_point: CoordPair,
626 mutation: Box<dyn BoxedMutation<Tile>>,
627 ) -> Result<(), Error> {
628 let curr_tile = self.get(canvas_point)?;
629 let new_tile = mutation
630 .mutate_boxed(curr_tile)
631 .map_err(|source| Error::TileMutation(canvas_point, source))?;
632 self.set(canvas_point, new_tile)?;
633 Ok(())
634 }
635
636 fn execute_clear_screen(&mut self, color: Color) -> Result<(), Error> {
637 for y in 0 .. self.canvas_size.y {
638 for x in 0 .. self.canvas_size.x {
639 self.set(
640 CoordPair { y, x },
641 Tile {
642 colors: ColorPair {
643 background: color,
644 foreground: color,
645 },
646 grapheme: ' '.into(),
647 },
648 )?;
649 }
650 }
651 Ok(())
652 }
653
654 fn point_to_index(
655 &self,
656 canvas_point: CoordPair,
657 ) -> Result<usize, InvalidCanvasPoint> {
658 let index = self
659 .canvas_size
660 .map(usize::from)
661 .as_rect_size(thedes_geometry::CoordPair::from_axes(|_| 0))
662 .checked_horz_area_down_to(canvas_point.map(usize::from))?;
663 Ok(index)
664 }
665
666 fn canvas_to_term(&self, canvas_point: CoordPair) -> CoordPair {
667 canvas_point + self.top_left_margin()
668 }
669}
670
671#[derive(Debug)]
672pub struct CanvasHandle {
673 size: CoordPair,
674 status: Status,
675 command_sender: non_blocking::spsc::unbounded::Sender<Vec<Command>>,
676 command_queue: Vec<Command>,
677}
678
679impl CanvasHandle {
680 fn new(
681 canvas_size: CoordPair,
682 status: Status,
683 command_sender: non_blocking::spsc::unbounded::Sender<Vec<Command>>,
684 ) -> Self {
685 Self {
686 size: canvas_size,
687 status,
688 command_sender,
689 command_queue: Vec::new(),
690 }
691 }
692
693 pub fn is_connected(&self) -> bool {
694 self.command_sender.is_connected()
695 }
696
697 pub fn size(&self) -> CoordPair {
698 self.size
699 }
700
701 pub fn queue<I>(&mut self, commands: I)
702 where
703 I: IntoIterator<Item = Command>,
704 {
705 self.command_queue.extend(commands);
706 }
707
708 pub fn flush(&mut self) -> Result<(), FlushError> {
709 let commands = mem::take(&mut self.command_queue);
710 self.command_sender.send(commands).map_err(FlushError::new)
711 }
712
713 pub fn is_blocked(&self) -> bool {
714 self.status.is_blocked()
715 }
716}
717
718#[cfg(test)]
719mod test {
720 use std::time::Duration;
721
722 use thedes_async_util::{non_blocking, timer::Timer};
723 use tokio::time::timeout;
724 use tokio_util::sync::CancellationToken;
725
726 use crate::{
727 color::{BasicColor, ColorPair},
728 geometry::CoordPair,
729 grapheme,
730 input::TermSizeWatch,
731 mutation::{MutationExt, Set},
732 runtime::JoinSet,
733 screen::{
734 Command,
735 Config,
736 OpenResources,
737 device::{self, mock::ScreenDeviceMock},
738 },
739 status::Status,
740 tile::{MutateColors, MutateGrapheme},
741 };
742
743 #[tokio::test(flavor = "multi_thread")]
744 async fn init_sends_command() {
745 let device_mock = ScreenDeviceMock::new(CoordPair { y: 40, x: 90 });
746 device_mock.enable_command_log();
747 let device = device_mock.open();
748
749 let timer = Timer::new(Duration::from_millis(4));
750 let mut tick_session = timer.new_session();
751 let cancel_token = CancellationToken::new();
752 let grapheme_registry = grapheme::Registry::new();
753 let (_term_size_sender, term_size_receiver) =
754 non_blocking::spsc::watch::channel();
755 let term_size_watch = TermSizeWatch::new(term_size_receiver);
756
757 let mut join_set = JoinSet::new();
758
759 let resources = OpenResources {
760 device,
761 timer,
762 cancel_token,
763 grapheme_registry,
764 term_size_watch,
765 status: Status::new(),
766 };
767
768 let handles = Config::new()
769 .with_canvas_size(CoordPair { y: 22, x: 78 })
770 .open(resources, &mut join_set);
771 tick_session.tick().await;
772 tick_session.tick().await;
773 drop(tick_session);
774 drop(handles);
775
776 let results = timeout(Duration::from_millis(200), join_set.join_all())
777 .await
778 .unwrap();
779 for result in results {
780 result.unwrap();
781 }
782
783 let command_log = device_mock.take_command_log().unwrap();
784 assert_ne!(command_log, &[] as &[Vec<device::Command>]);
785
786 assert!(
787 command_log.iter().flatten().next().is_some(),
788 "commands: {command_log:#?}",
789 );
790 }
791
792 #[tokio::test(flavor = "multi_thread")]
793 async fn init_flushes() {
794 let device_mock = ScreenDeviceMock::new(CoordPair { y: 40, x: 90 });
795 device_mock.enable_command_log();
796 let device = device_mock.open();
797
798 let timer = Timer::new(Duration::from_millis(4));
799 let mut tick_session = timer.new_session();
800 let cancel_token = CancellationToken::new();
801 let grapheme_registry = grapheme::Registry::new();
802 let (_term_size_sender, term_size_receiver) =
803 non_blocking::spsc::watch::channel();
804 let term_size_watch = TermSizeWatch::new(term_size_receiver);
805
806 let mut join_set = JoinSet::new();
807
808 let resources = OpenResources {
809 device,
810 timer,
811 cancel_token,
812 grapheme_registry,
813 term_size_watch,
814 status: Status::new(),
815 };
816
817 let handles = Config::new()
818 .with_canvas_size(CoordPair { y: 22, x: 78 })
819 .open(resources, &mut join_set);
820 tick_session.tick().await;
821 tick_session.tick().await;
822 drop(tick_session);
823 drop(handles);
824
825 let results = timeout(Duration::from_millis(200), join_set.join_all())
826 .await
827 .unwrap();
828 for result in results {
829 result.unwrap();
830 }
831
832 let command_log = device_mock.take_command_log().unwrap();
833 assert!(command_log.len() > 1);
834 assert!(
835 command_log.iter().flatten().next().is_some(),
836 "commands: {command_log:#?}",
837 );
838 }
839
840 #[tokio::test(flavor = "multi_thread")]
841 async fn mutation_sends_command() {
842 let device_mock = ScreenDeviceMock::new(CoordPair { y: 40, x: 90 });
843 device_mock.enable_command_log();
844 let device = device_mock.open();
845
846 let timer = Timer::new(Duration::from_millis(4));
847 let mut tick_session = timer.new_session();
848 let cancel_token = CancellationToken::new();
849 let grapheme_registry = grapheme::Registry::new();
850 let (_term_size_sender, term_size_receiver) =
851 non_blocking::spsc::watch::channel();
852 let term_size_watch = TermSizeWatch::new(term_size_receiver);
853
854 let mut join_set = JoinSet::new();
855
856 let resources = OpenResources {
857 device,
858 timer,
859 cancel_token,
860 grapheme_registry,
861 term_size_watch,
862 status: Status::new(),
863 };
864
865 let mut handles = Config::new()
866 .with_canvas_size(CoordPair { y: 22, x: 78 })
867 .open(resources, &mut join_set);
868 handles.canvas.queue([Command::new_mutation(
869 CoordPair { y: 12, x: 30 },
870 MutateColors(Set(ColorPair {
871 foreground: BasicColor::DarkCyan.into(),
872 background: BasicColor::LightGreen.into(),
873 }))
874 .then(MutateGrapheme(Set('B'.into()))),
875 )]);
876 handles.canvas.flush().unwrap();
877 tick_session.tick().await;
878 tick_session.tick().await;
879 drop(tick_session);
880 drop(handles);
881
882 let results = timeout(Duration::from_millis(200), join_set.join_all())
883 .await
884 .unwrap();
885 for result in results {
886 result.unwrap();
887 }
888
889 let command_log = device_mock.take_command_log().unwrap();
890
891 assert_eq!(
892 1,
893 command_log
894 .iter()
895 .flatten()
896 .filter(|command| **command == device::Command::Write('B'))
897 .count(),
898 "commands: {command_log:#?}",
899 );
900
901 assert_eq!(
902 1,
903 command_log
904 .iter()
905 .flatten()
906 .filter(|command| **command
907 == device::Command::SetForeground(
908 BasicColor::DarkCyan.into()
909 ))
910 .count(),
911 "commands: {command_log:#?}",
912 );
913
914 assert_eq!(
915 1,
916 command_log
917 .iter()
918 .flatten()
919 .filter(|command| **command
920 == device::Command::SetBackground(
921 BasicColor::LightGreen.into()
922 ))
923 .count(),
924 "commands: {command_log:#?}",
925 );
926 }
927
928 #[tokio::test(flavor = "multi_thread")]
929 async fn clear_screen_sends_command() {
930 let device_mock = ScreenDeviceMock::new(CoordPair { y: 40, x: 90 });
931 device_mock.enable_command_log();
932 let device = device_mock.open();
933
934 let timer = Timer::new(Duration::from_millis(4));
935 let mut tick_session = timer.new_session();
936 let cancel_token = CancellationToken::new();
937 let grapheme_registry = grapheme::Registry::new();
938 let (_term_size_sender, term_size_receiver) =
939 non_blocking::spsc::watch::channel();
940 let term_size_watch = TermSizeWatch::new(term_size_receiver);
941
942 let mut join_set = JoinSet::new();
943
944 let resources = OpenResources {
945 device,
946 timer,
947 cancel_token,
948 grapheme_registry,
949 term_size_watch,
950 status: Status::new(),
951 };
952
953 let mut handles = Config::new()
954 .with_canvas_size(CoordPair { y: 20, x: 60 })
955 .open(resources, &mut join_set);
956 handles
957 .canvas
958 .queue([Command::ClearScreen(BasicColor::DarkRed.into())]);
959 handles.canvas.flush().unwrap();
960 tick_session.tick().await;
961 tick_session.tick().await;
962 drop(tick_session);
963 drop(handles);
964
965 let results = timeout(Duration::from_millis(200), join_set.join_all())
966 .await
967 .unwrap();
968 for result in results {
969 result.unwrap();
970 }
971
972 let command_log = device_mock.take_command_log().unwrap();
973
974 assert_eq!(
975 20 * 60,
976 command_log
977 .iter()
978 .flatten()
979 .filter(|command| **command == device::Command::Write(' '))
980 .count(),
981 "commands: {command_log:#?}",
982 );
983
984 assert_eq!(
985 1,
986 command_log
987 .iter()
988 .flatten()
989 .filter(|command| **command
990 == device::Command::SetBackground(
991 BasicColor::DarkRed.into()
992 ))
993 .count(),
994 "commands: {command_log:#?}",
995 );
996 }
997
998 #[tokio::test(flavor = "multi_thread")]
999 async fn resize_too_small() {
1000 let device_mock = ScreenDeviceMock::new(CoordPair { y: 40, x: 90 });
1001 device_mock.enable_command_log();
1002 let device = device_mock.open();
1003
1004 let timer = Timer::new(Duration::from_millis(4));
1005 let mut tick_session = timer.new_session();
1006 let cancel_token = CancellationToken::new();
1007 let grapheme_registry = grapheme::Registry::new();
1008 let (mut term_size_sender, term_size_receiver) =
1009 non_blocking::spsc::watch::channel();
1010 let term_size_watch = TermSizeWatch::new(term_size_receiver);
1011
1012 let mut join_set = JoinSet::new();
1013
1014 let resources = OpenResources {
1015 device,
1016 timer,
1017 cancel_token,
1018 grapheme_registry,
1019 term_size_watch,
1020 status: Status::new(),
1021 };
1022
1023 let handles = Config::new()
1024 .with_canvas_size(CoordPair { y: 24, x: 80 })
1025 .open(resources, &mut join_set);
1026 term_size_sender.send(CoordPair { y: 26, x: 81 }).unwrap();
1027 tick_session.tick().await;
1028 tick_session.tick().await;
1029 assert!(handles.canvas.is_blocked());
1030 drop(tick_session);
1031 drop(handles);
1032
1033 let results = timeout(Duration::from_millis(200), join_set.join_all())
1034 .await
1035 .unwrap();
1036 for result in results {
1037 result.unwrap();
1038 }
1039
1040 let command_log = device_mock.take_command_log().unwrap();
1041
1042 let resize_msg = "RESIZE 82x26";
1043 for ch in resize_msg.chars() {
1044 assert_eq!(
1045 resize_msg.chars().filter(|resize_ch| *resize_ch == ch).count(),
1046 command_log
1047 .iter()
1048 .flatten()
1049 .filter(|command| **command == device::Command::Write(ch))
1050 .count(),
1051 "expected {ch} to occur once, commands: {command_log:#?}",
1052 );
1053 }
1054 }
1055
1056 #[tokio::test(flavor = "multi_thread")]
1057 async fn resize_min_size() {
1058 let device_mock = ScreenDeviceMock::new(CoordPair { y: 40, x: 90 });
1059 device_mock.enable_command_log();
1060 let device = device_mock.open();
1061
1062 let timer = Timer::new(Duration::from_millis(4));
1063 let mut tick_session = timer.new_session();
1064 let cancel_token = CancellationToken::new();
1065 let grapheme_registry = grapheme::Registry::new();
1066 let (mut term_size_sender, term_size_receiver) =
1067 non_blocking::spsc::watch::channel();
1068 let term_size_watch = TermSizeWatch::new(term_size_receiver);
1069
1070 let mut join_set = JoinSet::new();
1071
1072 let resources = OpenResources {
1073 device,
1074 timer,
1075 cancel_token,
1076 grapheme_registry,
1077 term_size_watch,
1078 status: Status::new(),
1079 };
1080
1081 let mut handles = Config::new()
1082 .with_canvas_size(CoordPair { y: 24, x: 80 })
1083 .open(resources, &mut join_set);
1084 term_size_sender.send(CoordPair { y: 26, x: 82 }).unwrap();
1085 handles.canvas.queue([Command::new_mutation(
1086 CoordPair { y: 12, x: 30 },
1087 MutateColors(Set(ColorPair {
1088 foreground: BasicColor::DarkCyan.into(),
1089 background: BasicColor::LightGreen.into(),
1090 }))
1091 .then(MutateGrapheme(Set('B'.into()))),
1092 )]);
1093 handles.canvas.flush().unwrap();
1094 tick_session.tick().await;
1095 tick_session.tick().await;
1096 drop(tick_session);
1097 drop(handles);
1098
1099 let results = timeout(Duration::from_millis(200), join_set.join_all())
1100 .await
1101 .unwrap();
1102 for result in results {
1103 result.unwrap();
1104 }
1105
1106 let command_log = device_mock.take_command_log().unwrap();
1107
1108 let resize_msg = "RESIZE 82x26";
1109 let mut occurences = 0;
1110
1111 for ch in resize_msg.chars() {
1112 if command_log
1113 .iter()
1114 .flatten()
1115 .filter(|command| **command == device::Command::Write(ch))
1116 .count()
1117 >= resize_msg
1118 .chars()
1119 .filter(|resize_ch| *resize_ch == ch)
1120 .count()
1121 {
1122 occurences += 1;
1123 }
1124 }
1125
1126 assert_ne!(occurences, resize_msg.len(), "commands: {command_log:#?}",);
1127 }
1128
1129 #[tokio::test(flavor = "multi_thread")]
1130 async fn mutation_sends_command_after_resize_correction() {
1131 let device_mock = ScreenDeviceMock::new(CoordPair { y: 40, x: 90 });
1132 device_mock.enable_command_log();
1133 let device = device_mock.open();
1134
1135 let timer = Timer::new(Duration::from_millis(4));
1136 let mut tick_session = timer.new_session();
1137 let cancel_token = CancellationToken::new();
1138 let grapheme_registry = grapheme::Registry::new();
1139 let (mut term_size_sender, term_size_receiver) =
1140 non_blocking::spsc::watch::channel();
1141 let term_size_watch = TermSizeWatch::new(term_size_receiver);
1142
1143 let mut join_set = JoinSet::new();
1144
1145 let resources = OpenResources {
1146 device,
1147 timer,
1148 cancel_token,
1149 grapheme_registry,
1150 term_size_watch,
1151 status: Status::new(),
1152 };
1153
1154 let mut handles = Config::new()
1155 .with_canvas_size(CoordPair { y: 24, x: 80 })
1156 .open(resources, &mut join_set);
1157 term_size_sender.send(CoordPair { y: 26, x: 81 }).unwrap();
1158 tick_session.tick().await;
1159 tick_session.tick().await;
1160 term_size_sender.send(CoordPair { y: 26, x: 82 }).unwrap();
1161 tick_session.tick().await;
1162 tick_session.tick().await;
1163 handles.canvas.queue([Command::new_mutation(
1164 CoordPair { y: 12, x: 30 },
1165 MutateColors(Set(ColorPair {
1166 foreground: BasicColor::DarkCyan.into(),
1167 background: BasicColor::LightGreen.into(),
1168 }))
1169 .then(MutateGrapheme(Set('B'.into()))),
1170 )]);
1171 handles.canvas.flush().unwrap();
1172 tick_session.tick().await;
1173 tick_session.tick().await;
1174 drop(tick_session);
1175 drop(handles);
1176
1177 let results = timeout(Duration::from_millis(200), join_set.join_all())
1178 .await
1179 .unwrap();
1180 for result in results {
1181 result.unwrap();
1182 }
1183
1184 let command_log = device_mock.take_command_log().unwrap();
1185
1186 assert_eq!(
1187 1,
1188 command_log
1189 .iter()
1190 .flatten()
1191 .filter(|command| **command == device::Command::Write('B'))
1192 .count(),
1193 "commands: {command_log:#?}",
1194 );
1195
1196 assert_eq!(
1197 1,
1198 command_log
1199 .iter()
1200 .flatten()
1201 .filter(|command| **command
1202 == device::Command::SetForeground(
1203 BasicColor::DarkCyan.into()
1204 ))
1205 .count(),
1206 "commands: {command_log:#?}",
1207 );
1208
1209 assert_eq!(
1210 1,
1211 command_log
1212 .iter()
1213 .flatten()
1214 .filter(|command| **command
1215 == device::Command::SetBackground(
1216 BasicColor::LightGreen.into()
1217 ))
1218 .count(),
1219 "commands: {command_log:#?}",
1220 );
1221 }
1222
1223 #[tokio::test(flavor = "multi_thread")]
1224 async fn cancel_token_stops() {
1225 let device_mock = ScreenDeviceMock::new(CoordPair { y: 40, x: 90 });
1226 device_mock.enable_command_log();
1227 let device = device_mock.open();
1228
1229 let timer = Timer::new(Duration::from_millis(4));
1230 let tick_session = timer.new_session();
1231 let cancel_token = CancellationToken::new();
1232 let grapheme_registry = grapheme::Registry::new();
1233 let (_term_size_sender, term_size_receiver) =
1234 non_blocking::spsc::watch::channel();
1235 let term_size_watch = TermSizeWatch::new(term_size_receiver);
1236
1237 let mut join_set = JoinSet::new();
1238
1239 let resources = OpenResources {
1240 device,
1241 timer,
1242 cancel_token: cancel_token.clone(),
1243 grapheme_registry,
1244 term_size_watch,
1245 status: Status::new(),
1246 };
1247
1248 let _handles = Config::new()
1249 .with_canvas_size(CoordPair { y: 22, x: 78 })
1250 .open(resources, &mut join_set);
1251 drop(tick_session);
1252 cancel_token.cancel();
1253
1254 let results = timeout(Duration::from_millis(200), join_set.join_all())
1255 .await
1256 .unwrap();
1257 for result in results {
1258 result.unwrap();
1259 }
1260 }
1261
1262 #[tokio::test(flavor = "multi_thread")]
1263 async fn drop_term_size_stops() {
1264 let device_mock = ScreenDeviceMock::new(CoordPair { y: 40, x: 90 });
1265 device_mock.enable_command_log();
1266 let device = device_mock.open();
1267
1268 let timer = Timer::new(Duration::from_millis(4));
1269 let tick_session = timer.new_session();
1270 let cancel_token = CancellationToken::new();
1271 let grapheme_registry = grapheme::Registry::new();
1272 let (term_size_sender, term_size_receiver) =
1273 non_blocking::spsc::watch::channel();
1274 let term_size_watch = TermSizeWatch::new(term_size_receiver);
1275
1276 let mut join_set = JoinSet::new();
1277
1278 let resources = OpenResources {
1279 device,
1280 timer,
1281 cancel_token: cancel_token.clone(),
1282 grapheme_registry,
1283 term_size_watch,
1284 status: Status::new(),
1285 };
1286
1287 let _handles = Config::new()
1288 .with_canvas_size(CoordPair { y: 22, x: 78 })
1289 .open(resources, &mut join_set);
1290 drop(tick_session);
1291 drop(term_size_sender);
1292
1293 let results = timeout(Duration::from_millis(200), join_set.join_all())
1294 .await
1295 .unwrap();
1296 for result in results {
1297 result.unwrap();
1298 }
1299 }
1300
1301 #[tokio::test(flavor = "multi_thread")]
1302 async fn drop_canvas_handle_stops() {
1303 let device_mock = ScreenDeviceMock::new(CoordPair { y: 40, x: 90 });
1304 device_mock.enable_command_log();
1305 let device = device_mock.open();
1306
1307 let timer = Timer::new(Duration::from_millis(4));
1308 let tick_session = timer.new_session();
1309 let cancel_token = CancellationToken::new();
1310 let grapheme_registry = grapheme::Registry::new();
1311 let (_term_size_sender, term_size_receiver) =
1312 non_blocking::spsc::watch::channel();
1313 let term_size_watch = TermSizeWatch::new(term_size_receiver);
1314
1315 let mut join_set = JoinSet::new();
1316
1317 let resources = OpenResources {
1318 device,
1319 timer,
1320 cancel_token: cancel_token.clone(),
1321 grapheme_registry,
1322 term_size_watch,
1323 status: Status::new(),
1324 };
1325
1326 let handles = Config::new()
1327 .with_canvas_size(CoordPair { y: 22, x: 78 })
1328 .open(resources, &mut join_set);
1329 drop(tick_session);
1330 drop(handles.canvas);
1331
1332 let results = timeout(Duration::from_millis(200), join_set.join_all())
1333 .await
1334 .unwrap();
1335 for result in results {
1336 result.unwrap();
1337 }
1338 }
1339}