1use crate::Sink;
6use std::io;
7
8use katui_proto::probe::{Capabilities, CellPixels};
9
10use crate::select::{Backend, select};
11use katui_scene::placement::CellRect;
12use katui_scene::surface::Surface;
13
14use super::{AnimatedKitty, BlocksSurface, PatchedKitty, SixelSurface};
15
16#[derive(Debug)]
18pub(super) enum PixelStrategy {
19 Patched(PatchedKitty),
21 Animated(AnimatedKitty),
23 Sixel(SixelSurface),
25}
26
27#[derive(Debug, Default)]
30pub struct PixelScreen {
31 graphics: Option<PixelStrategy>,
32 blocks: BlocksSurface,
33}
34
35impl PixelScreen {
36 #[must_use]
40 pub fn new(caps: &Capabilities, cell: CellPixels, patched: PatchedKitty) -> Self {
41 let patched = if caps.multiplexed() {
42 patched.for_multiplexer(caps.id_space, caps.relative_placements)
43 } else {
44 patched
45 };
46 let graphics = match select(caps) {
47 Backend::Kitty => Some(PixelStrategy::Patched(patched)),
48 Backend::Sixel => Some(PixelStrategy::Sixel(SixelSurface::new(cell))),
49 Backend::Blocks => None,
50 };
51 Self {
52 graphics,
53 blocks: BlocksSurface::new(),
54 }
55 }
56
57 #[must_use]
60 pub fn picture(caps: &Capabilities, cell: CellPixels) -> Self {
61 let graphics =
62 (select(caps) == Backend::Sixel).then(|| PixelStrategy::Sixel(SixelSurface::new(cell)));
63 Self {
64 graphics,
65 blocks: BlocksSurface::new(),
66 }
67 }
68
69 #[must_use]
72 pub fn rows_shown(&self, rows: u16) -> u16 {
73 match self.graphics {
74 Some(PixelStrategy::Sixel(_)) => rows.saturating_sub(1),
75 _ => rows,
76 }
77 }
78
79 #[must_use]
83 pub fn animated(
84 caps: &Capabilities,
85 cell: CellPixels,
86 animated: AnimatedKitty,
87 fallback: PatchedKitty,
88 ) -> Self {
89 if select(caps) == Backend::Kitty && caps.animation && !caps.multiplexed() {
90 Self {
91 graphics: Some(PixelStrategy::Animated(animated)),
92 blocks: BlocksSurface::new(),
93 }
94 } else {
95 Self::new(caps, cell, fallback)
96 }
97 }
98
99 pub fn set_cell(&mut self, cell: CellPixels) {
101 if let Some(PixelStrategy::Patched(patched)) = &mut self.graphics {
102 patched.set_cell(cell);
103 }
104 }
105
106 pub fn repaint(&mut self) {
109 match &mut self.graphics {
110 Some(PixelStrategy::Patched(patched)) => patched.repaint(),
111 Some(PixelStrategy::Animated(animated)) => animated.repaint(),
112 Some(PixelStrategy::Sixel(sixel)) => sixel.repaint(),
113 None => {}
114 }
115 }
116
117 pub fn free<W: Sink>(&mut self, out: &mut W) -> io::Result<()> {
120 match &mut self.graphics {
121 Some(PixelStrategy::Patched(patched)) => patched.free(out),
122 Some(PixelStrategy::Animated(animated)) => animated.free(out),
123 Some(PixelStrategy::Sixel(sixel)) => {
124 sixel.repaint();
125 Ok(())
126 }
127 None => Ok(()),
128 }
129 }
130
131 pub fn render_frame<W: Sink>(
135 &mut self,
136 out: &mut W,
137 surface: &Surface,
138 columns: u16,
139 rows: u16,
140 ) -> io::Result<usize> {
141 match &mut self.graphics {
142 Some(PixelStrategy::Patched(patched)) => {
143 patched.render_frame(out, surface, CellRect::full(columns, rows))
144 }
145 Some(PixelStrategy::Animated(animated)) => {
146 animated.render_frame(out, surface, CellRect::full(columns, rows))
147 }
148 Some(PixelStrategy::Sixel(sixel)) => sixel.render_frame(out, surface, columns, rows),
149 None => {
150 let (width, height) = (u32::from(columns) * 2, u32::from(rows) * 2);
151 if (surface.width(), surface.height()) == (width, height) {
152 self.blocks.render_frame(out, surface)
153 } else {
154 surface
155 .scaled(width, height)
156 .map_or(Ok(0), |fitted| self.blocks.render_frame(out, &fitted))
157 }
158 }
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::PixelScreen;
166 use katui_proto::probe::{Capabilities, CellPixels, ProbeSource};
167 use katui_scene::surface::{Rgba, Surface};
168
169 const CELL: CellPixels = CellPixels {
170 width: 2,
171 height: 4,
172 };
173
174 fn caps(kitty: bool) -> Capabilities {
175 Capabilities {
176 kitty,
177 animation: false,
178 sixel: false,
179 truecolor: false,
180 identity: None,
181 cell: None,
182 text_area: None,
183 relative_placements: false,
184 local_files: false,
185 id_space: katui_proto::kitty::IdSpace::default(),
186 trust: ProbeSource::Sniffed,
187 }
188 }
189
190 fn plain(width: u32, height: u32) -> Surface {
191 crate::surface::test_support::solid(width, height, Rgba::solid(255, 80, 40))
192 }
193
194 #[test]
195 fn a_sixel_picture_covers_every_row_but_the_last_and_blocks_cover_all() {
196 let sixel = Capabilities {
197 sixel: true,
198 ..caps(false)
199 };
200 assert_eq!(PixelScreen::picture(&sixel, CELL).rows_shown(24), 23);
201 assert_eq!(PixelScreen::picture(&caps(false), CELL).rows_shown(24), 24);
202 }
203
204 #[test]
205 fn a_kitty_terminal_gets_graphics_frames() {
206 let patched = crate::surface::PatchedKitty::new(
207 90,
208 CellPixels {
209 width: 2,
210 height: 4,
211 },
212 );
213 let mut screen = PixelScreen::new(&caps(true), CELL, patched);
214 let mut out = Vec::new();
215 screen
216 .render_frame(&mut out, &plain(8, 8), 4, 2)
217 .expect("writing to a Vec cannot fail");
218 let text = String::from_utf8_lossy(&out);
219 assert!(text.contains("a=t"), "the surface transmits as an image");
220 assert!(text.contains("a=p"), "and is placed");
221 }
222
223 #[test]
224 fn a_sixel_terminal_gets_sixel_tiles() {
225 let patched = crate::surface::PatchedKitty::new(
226 90,
227 CellPixels {
228 width: 2,
229 height: 4,
230 },
231 );
232 let mut sixel = caps(false);
233 sixel.sixel = true;
234 let mut screen = PixelScreen::new(&sixel, CELL, patched);
235 let mut out = Vec::new();
236 screen
237 .render_frame(&mut out, &plain(8, 4), 4, 2)
238 .expect("writing to a Vec cannot fail");
239 assert!(String::from_utf8_lossy(&out).contains("\x1bP0;1;0q"));
240 }
241
242 #[test]
243 fn a_plain_terminal_gets_quarter_blocks() {
244 let patched = crate::surface::PatchedKitty::new(
245 90,
246 CellPixels {
247 width: 2,
248 height: 4,
249 },
250 );
251 let mut screen = PixelScreen::new(&caps(false), CELL, patched);
252 let mut out = Vec::new();
253 screen
254 .render_frame(&mut out, &plain(8, 4), 4, 2)
255 .expect("writing to a Vec cannot fail");
256 let text = String::from_utf8_lossy(&out);
257 assert!(!text.contains("a=t"), "no graphics on a plain terminal");
258 assert!(text.contains("\x1b[38;2;"), "quarter blocks render");
259 }
260
261 #[test]
262 fn an_animation_averse_graphics_terminal_gets_patches() {
263 let mut no_animation = caps(true);
264 no_animation.animation = false;
265 let animated = crate::surface::AnimatedKitty::new(90, 20, 8, 16);
266 let fallback = crate::surface::PatchedKitty::new(
267 90,
268 CellPixels {
269 width: 2,
270 height: 4,
271 },
272 );
273 let mut screen = PixelScreen::animated(&no_animation, CELL, animated, fallback);
274 let mut out = Vec::new();
275 screen
276 .render_frame(&mut out, &plain(8, 8), 4, 2)
277 .expect("writing to a Vec cannot fail");
278 let text = String::from_utf8_lossy(&out);
279 assert!(!text.contains("a=f"), "no animation frames");
280 assert!(text.contains("a=t"), "patch transmits carry the surface");
281 }
282
283 #[test]
284 fn an_animation_capable_terminal_gets_frames() {
285 let mut animated_caps = caps(true);
286 animated_caps.animation = true;
287 let animated = crate::surface::AnimatedKitty::new(90, 20, 8, 16);
288 let fallback = crate::surface::PatchedKitty::new(
289 90,
290 CellPixels {
291 width: 2,
292 height: 4,
293 },
294 );
295 let mut screen = PixelScreen::animated(&animated_caps, CELL, animated, fallback);
296 let mut out = Vec::new();
297 screen
298 .render_frame(&mut out, &plain(8, 8), 4, 2)
299 .expect("writing to a Vec cannot fail");
300 let text = String::from_utf8_lossy(&out);
301 assert!(text.contains("a=a"), "playback is terminal-driven");
302 }
303}