katui_present/surface/
sixel.rs1use std::io::{self, Write as _};
5
6use katui_proto::probe::CellPixels;
7use katui_proto::sixel;
8use katui_scene::cell::Position;
9use katui_scene::surface::{PixelRect, Surface, diff_tiles};
10
11use super::damage_is_dense;
12use super::patched::tile_pixels;
13use crate::Sink;
14use crate::cells::move_cursor;
15
16#[derive(Debug)]
20pub struct SixelSurface {
21 cell: CellPixels,
22 prev: Option<Surface>,
23}
24
25impl SixelSurface {
26 #[must_use]
28 pub fn new(cell: CellPixels) -> Self {
29 Self { cell, prev: None }
30 }
31
32 pub fn repaint(&mut self) {
34 self.prev = None;
35 }
36
37 pub fn render_frame<W: Sink>(
40 &mut self,
41 out: &mut W,
42 surface: &Surface,
43 columns: u16,
44 rows: u16,
45 ) -> io::Result<usize> {
46 let width = u32::from(columns) * u32::from(self.cell.width);
47 let height = u32::from(rows.saturating_sub(1)) * u32::from(self.cell.height);
48 let Some(screen) = surface.scaled(width, height) else {
49 return Ok(0);
50 };
51 let tile_side = u32::from(self.cell.width) * u32::from(self.cell.height);
52 let damage = self.prev.as_ref().map_or_else(
53 || vec![whole(width, height)],
54 |prev| diff_tiles(prev, &screen, tile_side),
55 );
56 let damage = if damage_is_dense(&screen, tile_side, damage.len()) {
57 vec![whole(width, height)]
58 } else {
59 damage
60 };
61 let mut bytes = Vec::new();
62 if !damage.is_empty() {
63 bytes.write_all(b"\x1b[?2026h")?;
64 for rect in &damage {
65 self.write_tile(&mut bytes, &screen, *rect)?;
66 }
67 bytes.write_all(b"\x1b[?2026l")?;
68 }
69 self.prev = Some(screen);
70 out.write_all(&bytes)?;
71 Ok(bytes.len())
72 }
73
74 fn write_tile(&self, bytes: &mut Vec<u8>, screen: &Surface, rect: PixelRect) -> io::Result<()> {
75 let pixels = tile_pixels(screen, rect);
76 let Some(image) = sixel::encode(&pixels, rect.width, rect.height) else {
77 return Ok(());
78 };
79 let at = Position {
80 x: u16::try_from(rect.x / u32::from(self.cell.width)).unwrap_or(u16::MAX),
81 y: u16::try_from(rect.y / u32::from(self.cell.height)).unwrap_or(u16::MAX),
82 };
83 move_cursor(bytes, at)?;
84 bytes.write_all(&image)
85 }
86}
87
88fn whole(width: u32, height: u32) -> PixelRect {
89 PixelRect {
90 x: 0,
91 y: 0,
92 width,
93 height,
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use katui_proto::probe::CellPixels;
100 use katui_scene::surface::{PixelPos, Rgba};
101
102 use super::SixelSurface;
103 use crate::surface::test_support::solid;
104
105 const CELL: CellPixels = CellPixels {
106 width: 4,
107 height: 6,
108 };
109
110 #[test]
111 fn the_first_frame_fills_the_screen_but_the_bottom_row() {
112 let mut presenter = SixelSurface::new(CELL);
113 let mut out = Vec::new();
114 presenter
115 .render_frame(&mut out, &solid(10, 10, Rgba::solid(255, 0, 0)), 20, 6)
116 .expect("writing to a Vec cannot fail");
117 let text = String::from_utf8_lossy(&out);
118 assert!(
119 text.contains("\x1b[1;1H\x1bP0;1;0q\"1;1;80;30"),
120 "80x30 pixels from the top left: {text:.60}"
121 );
122 assert_eq!(text.matches("\x1bP").count(), 1, "one image");
123 }
124
125 #[test]
126 fn a_changed_pixel_redraws_one_tile_at_its_cell() {
127 let mut presenter = SixelSurface::new(CELL);
128 let mut surface = solid(80, 60, Rgba::solid(0, 0, 0));
129 let mut out = Vec::new();
130 presenter
131 .render_frame(&mut out, &surface, 20, 11)
132 .expect("writing to a Vec cannot fail");
133 surface.set(PixelPos::new(50, 30), Rgba::solid(255, 255, 255));
134 out.clear();
135 presenter
136 .render_frame(&mut out, &surface, 20, 11)
137 .expect("writing to a Vec cannot fail");
138 let text = String::from_utf8_lossy(&out);
139 assert_eq!(text.matches("\x1bP").count(), 1, "one tile");
140 assert!(
141 text.contains("\x1b[5;13H\x1bP0;1;0q\"1;1;24;24"),
142 "the 24 pixel tile at x 48, y 24: {text:.80}"
143 );
144 out.clear();
145 let written = presenter
146 .render_frame(&mut out, &surface, 20, 11)
147 .expect("writing to a Vec cannot fail");
148 assert_eq!(written, 0, "an unchanged frame writes nothing");
149 }
150}