use crate::Sink;
use std::io;
use katui_proto::probe::{Capabilities, CellPixels};
use crate::select::{Backend, select};
use katui_scene::placement::CellRect;
use katui_scene::surface::Surface;
use super::{AnimatedKitty, BlocksSurface, PatchedKitty, SixelSurface};
#[derive(Debug)]
pub(super) enum PixelStrategy {
Patched(PatchedKitty),
Animated(AnimatedKitty),
Sixel(SixelSurface),
}
#[derive(Debug, Default)]
pub struct PixelScreen {
graphics: Option<PixelStrategy>,
blocks: BlocksSurface,
}
impl PixelScreen {
#[must_use]
pub fn new(caps: &Capabilities, cell: CellPixels, patched: PatchedKitty) -> Self {
let patched = if caps.multiplexed() {
patched.for_multiplexer(caps.id_space, caps.relative_placements)
} else {
patched
};
let graphics = match select(caps) {
Backend::Kitty => Some(PixelStrategy::Patched(patched)),
Backend::Sixel => Some(PixelStrategy::Sixel(SixelSurface::new(cell))),
Backend::Blocks => None,
};
Self {
graphics,
blocks: BlocksSurface::new(),
}
}
#[must_use]
pub fn picture(caps: &Capabilities, cell: CellPixels) -> Self {
let graphics =
(select(caps) == Backend::Sixel).then(|| PixelStrategy::Sixel(SixelSurface::new(cell)));
Self {
graphics,
blocks: BlocksSurface::new(),
}
}
#[must_use]
pub fn rows_shown(&self, rows: u16) -> u16 {
match self.graphics {
Some(PixelStrategy::Sixel(_)) => rows.saturating_sub(1),
_ => rows,
}
}
#[must_use]
pub fn animated(
caps: &Capabilities,
cell: CellPixels,
animated: AnimatedKitty,
fallback: PatchedKitty,
) -> Self {
if select(caps) == Backend::Kitty && caps.animation && !caps.multiplexed() {
Self {
graphics: Some(PixelStrategy::Animated(animated)),
blocks: BlocksSurface::new(),
}
} else {
Self::new(caps, cell, fallback)
}
}
pub fn set_cell(&mut self, cell: CellPixels) {
if let Some(PixelStrategy::Patched(patched)) = &mut self.graphics {
patched.set_cell(cell);
}
}
pub fn repaint(&mut self) {
match &mut self.graphics {
Some(PixelStrategy::Patched(patched)) => patched.repaint(),
Some(PixelStrategy::Animated(animated)) => animated.repaint(),
Some(PixelStrategy::Sixel(sixel)) => sixel.repaint(),
None => {}
}
}
pub fn free<W: Sink>(&mut self, out: &mut W) -> io::Result<()> {
match &mut self.graphics {
Some(PixelStrategy::Patched(patched)) => patched.free(out),
Some(PixelStrategy::Animated(animated)) => animated.free(out),
Some(PixelStrategy::Sixel(sixel)) => {
sixel.repaint();
Ok(())
}
None => Ok(()),
}
}
pub fn render_frame<W: Sink>(
&mut self,
out: &mut W,
surface: &Surface,
columns: u16,
rows: u16,
) -> io::Result<usize> {
match &mut self.graphics {
Some(PixelStrategy::Patched(patched)) => {
patched.render_frame(out, surface, CellRect::full(columns, rows))
}
Some(PixelStrategy::Animated(animated)) => {
animated.render_frame(out, surface, CellRect::full(columns, rows))
}
Some(PixelStrategy::Sixel(sixel)) => sixel.render_frame(out, surface, columns, rows),
None => {
let (width, height) = (u32::from(columns) * 2, u32::from(rows) * 2);
if (surface.width(), surface.height()) == (width, height) {
self.blocks.render_frame(out, surface)
} else {
surface
.scaled(width, height)
.map_or(Ok(0), |fitted| self.blocks.render_frame(out, &fitted))
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::PixelScreen;
use katui_proto::probe::{Capabilities, CellPixels, ProbeSource};
use katui_scene::surface::{Rgba, Surface};
const CELL: CellPixels = CellPixels {
width: 2,
height: 4,
};
fn caps(kitty: bool) -> Capabilities {
Capabilities {
kitty,
animation: false,
sixel: false,
truecolor: false,
identity: None,
cell: None,
text_area: None,
relative_placements: false,
id_space: katui_proto::kitty::IdSpace::default(),
trust: ProbeSource::Sniffed,
}
}
fn plain(width: u32, height: u32) -> Surface {
crate::surface::test_support::solid(width, height, Rgba::solid(255, 80, 40))
}
#[test]
fn a_sixel_picture_covers_every_row_but_the_last_and_blocks_cover_all() {
let sixel = Capabilities {
sixel: true,
..caps(false)
};
assert_eq!(PixelScreen::picture(&sixel, CELL).rows_shown(24), 23);
assert_eq!(PixelScreen::picture(&caps(false), CELL).rows_shown(24), 24);
}
#[test]
fn a_kitty_terminal_gets_graphics_frames() {
let patched = crate::surface::PatchedKitty::new(
90,
CellPixels {
width: 2,
height: 4,
},
);
let mut screen = PixelScreen::new(&caps(true), CELL, patched);
let mut out = Vec::new();
screen
.render_frame(&mut out, &plain(8, 8), 4, 2)
.expect("writing to a Vec cannot fail");
let text = String::from_utf8_lossy(&out);
assert!(text.contains("a=t"), "the surface transmits as an image");
assert!(text.contains("a=p"), "and is placed");
}
#[test]
fn a_sixel_terminal_gets_sixel_tiles() {
let patched = crate::surface::PatchedKitty::new(
90,
CellPixels {
width: 2,
height: 4,
},
);
let mut sixel = caps(false);
sixel.sixel = true;
let mut screen = PixelScreen::new(&sixel, CELL, patched);
let mut out = Vec::new();
screen
.render_frame(&mut out, &plain(8, 4), 4, 2)
.expect("writing to a Vec cannot fail");
assert!(String::from_utf8_lossy(&out).contains("\x1bP0;1;0q"));
}
#[test]
fn a_plain_terminal_gets_quarter_blocks() {
let patched = crate::surface::PatchedKitty::new(
90,
CellPixels {
width: 2,
height: 4,
},
);
let mut screen = PixelScreen::new(&caps(false), CELL, patched);
let mut out = Vec::new();
screen
.render_frame(&mut out, &plain(8, 4), 4, 2)
.expect("writing to a Vec cannot fail");
let text = String::from_utf8_lossy(&out);
assert!(!text.contains("a=t"), "no graphics on a plain terminal");
assert!(text.contains("\x1b[38;2;"), "quarter blocks render");
}
#[test]
fn an_animation_averse_graphics_terminal_gets_patches() {
let mut no_animation = caps(true);
no_animation.animation = false;
let animated = crate::surface::AnimatedKitty::new(90, 20, 8, 16);
let fallback = crate::surface::PatchedKitty::new(
90,
CellPixels {
width: 2,
height: 4,
},
);
let mut screen = PixelScreen::animated(&no_animation, CELL, animated, fallback);
let mut out = Vec::new();
screen
.render_frame(&mut out, &plain(8, 8), 4, 2)
.expect("writing to a Vec cannot fail");
let text = String::from_utf8_lossy(&out);
assert!(!text.contains("a=f"), "no animation frames");
assert!(text.contains("a=t"), "patch transmits carry the surface");
}
#[test]
fn an_animation_capable_terminal_gets_frames() {
let mut animated_caps = caps(true);
animated_caps.animation = true;
let animated = crate::surface::AnimatedKitty::new(90, 20, 8, 16);
let fallback = crate::surface::PatchedKitty::new(
90,
CellPixels {
width: 2,
height: 4,
},
);
let mut screen = PixelScreen::animated(&animated_caps, CELL, animated, fallback);
let mut out = Vec::new();
screen
.render_frame(&mut out, &plain(8, 8), 4, 2)
.expect("writing to a Vec cannot fail");
let text = String::from_utf8_lossy(&out);
assert!(text.contains("a=a"), "playback is terminal-driven");
}
}