use std::io::{self, Write as _};
use katui_proto::probe::CellPixels;
use katui_proto::sixel;
use katui_scene::cell::Position;
use katui_scene::surface::{PixelRect, Surface, diff_tiles};
use super::damage_is_dense;
use super::patched::tile_pixels;
use crate::Sink;
use crate::cells::move_cursor;
#[derive(Debug)]
pub struct SixelSurface {
cell: CellPixels,
prev: Option<Surface>,
}
impl SixelSurface {
#[must_use]
pub fn new(cell: CellPixels) -> Self {
Self { cell, prev: None }
}
pub fn repaint(&mut self) {
self.prev = None;
}
pub fn render_frame<W: Sink>(
&mut self,
out: &mut W,
surface: &Surface,
columns: u16,
rows: u16,
) -> io::Result<usize> {
let width = u32::from(columns) * u32::from(self.cell.width);
let height = u32::from(rows.saturating_sub(1)) * u32::from(self.cell.height);
let Some(screen) = surface.scaled(width, height) else {
return Ok(0);
};
let tile_side = u32::from(self.cell.width) * u32::from(self.cell.height);
let damage = self.prev.as_ref().map_or_else(
|| vec![whole(width, height)],
|prev| diff_tiles(prev, &screen, tile_side),
);
let damage = if damage_is_dense(&screen, tile_side, damage.len()) {
vec![whole(width, height)]
} else {
damage
};
let mut bytes = Vec::new();
if !damage.is_empty() {
bytes.write_all(b"\x1b[?2026h")?;
for rect in &damage {
self.write_tile(&mut bytes, &screen, *rect)?;
}
bytes.write_all(b"\x1b[?2026l")?;
}
self.prev = Some(screen);
out.write_all(&bytes)?;
Ok(bytes.len())
}
fn write_tile(&self, bytes: &mut Vec<u8>, screen: &Surface, rect: PixelRect) -> io::Result<()> {
let pixels = tile_pixels(screen, rect);
let Some(image) = sixel::encode(&pixels, rect.width, rect.height) else {
return Ok(());
};
let at = Position {
x: u16::try_from(rect.x / u32::from(self.cell.width)).unwrap_or(u16::MAX),
y: u16::try_from(rect.y / u32::from(self.cell.height)).unwrap_or(u16::MAX),
};
move_cursor(bytes, at)?;
bytes.write_all(&image)
}
}
fn whole(width: u32, height: u32) -> PixelRect {
PixelRect {
x: 0,
y: 0,
width,
height,
}
}
#[cfg(test)]
mod tests {
use katui_proto::probe::CellPixels;
use katui_scene::surface::{PixelPos, Rgba};
use super::SixelSurface;
use crate::surface::test_support::solid;
const CELL: CellPixels = CellPixels {
width: 4,
height: 6,
};
#[test]
fn the_first_frame_fills_the_screen_but_the_bottom_row() {
let mut presenter = SixelSurface::new(CELL);
let mut out = Vec::new();
presenter
.render_frame(&mut out, &solid(10, 10, Rgba::solid(255, 0, 0)), 20, 6)
.expect("writing to a Vec cannot fail");
let text = String::from_utf8_lossy(&out);
assert!(
text.contains("\x1b[1;1H\x1bP0;1;0q\"1;1;80;30"),
"80x30 pixels from the top left: {text:.60}"
);
assert_eq!(text.matches("\x1bP").count(), 1, "one image");
}
#[test]
fn a_changed_pixel_redraws_one_tile_at_its_cell() {
let mut presenter = SixelSurface::new(CELL);
let mut surface = solid(80, 60, Rgba::solid(0, 0, 0));
let mut out = Vec::new();
presenter
.render_frame(&mut out, &surface, 20, 11)
.expect("writing to a Vec cannot fail");
surface.set(PixelPos::new(50, 30), Rgba::solid(255, 255, 255));
out.clear();
presenter
.render_frame(&mut out, &surface, 20, 11)
.expect("writing to a Vec cannot fail");
let text = String::from_utf8_lossy(&out);
assert_eq!(text.matches("\x1bP").count(), 1, "one tile");
assert!(
text.contains("\x1b[5;13H\x1bP0;1;0q\"1;1;24;24"),
"the 24 pixel tile at x 48, y 24: {text:.80}"
);
out.clear();
let written = presenter
.render_frame(&mut out, &surface, 20, 11)
.expect("writing to a Vec cannot fail");
assert_eq!(written, 0, "an unchanged frame writes nothing");
}
}