Skip to main content

katui_present/surface/
full.rs

1//! The whole-frame kitty strategy: every frame travels complete, under
2//! alternating image ids.
3
4use crate::Sink;
5use std::io::{self, Write as _};
6
7use katui_proto::kitty::{Delete, ImageId as KittyImage, PlacementId as KittyPlacement};
8use katui_scene::placement::CellRect;
9use katui_scene::surface::Surface;
10
11use super::write_whole_image;
12
13/// The full-frame kitty surface presenter: every frame transmits the
14/// whole surface compressed under one of two alternating image ids and
15/// places it, deleting the previous frame's placement, so the terminal
16/// never shows a half-updated image.
17#[derive(Debug)]
18pub struct KittySurface {
19    base: u32,
20    parity: bool,
21}
22
23impl KittySurface {
24    /// A presenter handing out image ids from `base` upward; pick a base
25    /// no other katui presenter in the session uses.
26    #[must_use]
27    pub fn new(base: u32) -> Self {
28        Self {
29            base,
30            parity: false,
31        }
32    }
33
34    /// Write `surface` scaled into `area` cells as one synchronized
35    /// frame and return the bytes written.
36    pub fn render_frame<W: Sink>(
37        &mut self,
38        out: &mut W,
39        surface: &Surface,
40        area: CellRect,
41    ) -> io::Result<usize> {
42        let current = self.base + u32::from(self.parity);
43        let previous = self.base + u32::from(!self.parity);
44        let mut bytes = Vec::new();
45        bytes.write_all(b"\x1b[?2026h")?;
46        write_whole_image(&mut bytes, KittyImage(current), surface, area)?;
47        bytes.write_all(&Delete::Placement(KittyImage(previous), KittyPlacement(1)).encode())?;
48        bytes.write_all(b"\x1b[?2026l")?;
49        self.parity = !self.parity;
50        let written = bytes.len();
51        out.write_all(&bytes)?;
52        Ok(written)
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::KittySurface;
59    use crate::surface::test_support::solid;
60    use katui_scene::placement::CellRect;
61    use katui_scene::surface::Rgba;
62
63    fn area() -> CellRect {
64        CellRect {
65            x: 2,
66            y: 1,
67            columns: 20,
68            rows: 10,
69        }
70    }
71
72    #[test]
73    fn frames_alternate_ids_and_delete_the_previous_frame() {
74        let surface = solid(4, 4, Rgba::solid(255, 0, 0));
75        let mut presenter = KittySurface::new(90);
76        let mut first = Vec::new();
77        presenter
78            .render_frame(&mut first, &surface, area())
79            .expect("writing to a Vec cannot fail");
80        let text = String::from_utf8_lossy(&first);
81        assert!(text.contains("i=90,"), "the first frame uses the base id");
82        assert!(text.contains("d=i,i=91"), "the other id's placement clears");
83        assert!(text.contains("o=z"), "frames travel compressed");
84
85        let mut second = Vec::new();
86        presenter
87            .render_frame(&mut second, &surface, area())
88            .expect("writing to a Vec cannot fail");
89        let text = String::from_utf8_lossy(&second);
90        assert!(text.contains("i=91,"), "the second frame alternates");
91        assert!(
92            text.contains("d=i,i=90"),
93            "the first frame's placement clears"
94        );
95    }
96
97    #[test]
98    fn frames_stay_inside_synchronized_output() {
99        let surface = solid(2, 2, Rgba::solid(0, 255, 0));
100        let mut presenter = KittySurface::new(10);
101        let mut out = Vec::new();
102        presenter
103            .render_frame(&mut out, &surface, area())
104            .expect("writing to a Vec cannot fail");
105        let text = String::from_utf8_lossy(&out);
106        assert!(text.starts_with("\x1b[?2026h"));
107        assert!(text.ends_with("\x1b[?2026l"));
108    }
109}