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 id_space: katui_proto::kitty::IdSpace::default(),
185 trust: ProbeSource::Sniffed,
186 }
187 }
188
189 fn plain(width: u32, height: u32) -> Surface {
190 crate::surface::test_support::solid(width, height, Rgba::solid(255, 80, 40))
191 }
192
193 #[test]
194 fn a_sixel_picture_covers_every_row_but_the_last_and_blocks_cover_all() {
195 let sixel = Capabilities {
196 sixel: true,
197 ..caps(false)
198 };
199 assert_eq!(PixelScreen::picture(&sixel, CELL).rows_shown(24), 23);
200 assert_eq!(PixelScreen::picture(&caps(false), CELL).rows_shown(24), 24);
201 }
202
203 #[test]
204 fn a_kitty_terminal_gets_graphics_frames() {
205 let patched = crate::surface::PatchedKitty::new(
206 90,
207 CellPixels {
208 width: 2,
209 height: 4,
210 },
211 );
212 let mut screen = PixelScreen::new(&caps(true), CELL, patched);
213 let mut out = Vec::new();
214 screen
215 .render_frame(&mut out, &plain(8, 8), 4, 2)
216 .expect("writing to a Vec cannot fail");
217 let text = String::from_utf8_lossy(&out);
218 assert!(text.contains("a=t"), "the surface transmits as an image");
219 assert!(text.contains("a=p"), "and is placed");
220 }
221
222 #[test]
223 fn a_sixel_terminal_gets_sixel_tiles() {
224 let patched = crate::surface::PatchedKitty::new(
225 90,
226 CellPixels {
227 width: 2,
228 height: 4,
229 },
230 );
231 let mut sixel = caps(false);
232 sixel.sixel = true;
233 let mut screen = PixelScreen::new(&sixel, CELL, patched);
234 let mut out = Vec::new();
235 screen
236 .render_frame(&mut out, &plain(8, 4), 4, 2)
237 .expect("writing to a Vec cannot fail");
238 assert!(String::from_utf8_lossy(&out).contains("\x1bP0;1;0q"));
239 }
240
241 #[test]
242 fn a_plain_terminal_gets_quarter_blocks() {
243 let patched = crate::surface::PatchedKitty::new(
244 90,
245 CellPixels {
246 width: 2,
247 height: 4,
248 },
249 );
250 let mut screen = PixelScreen::new(&caps(false), CELL, patched);
251 let mut out = Vec::new();
252 screen
253 .render_frame(&mut out, &plain(8, 4), 4, 2)
254 .expect("writing to a Vec cannot fail");
255 let text = String::from_utf8_lossy(&out);
256 assert!(!text.contains("a=t"), "no graphics on a plain terminal");
257 assert!(text.contains("\x1b[38;2;"), "quarter blocks render");
258 }
259
260 #[test]
261 fn an_animation_averse_graphics_terminal_gets_patches() {
262 let mut no_animation = caps(true);
263 no_animation.animation = false;
264 let animated = crate::surface::AnimatedKitty::new(90, 20, 8, 16);
265 let fallback = crate::surface::PatchedKitty::new(
266 90,
267 CellPixels {
268 width: 2,
269 height: 4,
270 },
271 );
272 let mut screen = PixelScreen::animated(&no_animation, CELL, animated, fallback);
273 let mut out = Vec::new();
274 screen
275 .render_frame(&mut out, &plain(8, 8), 4, 2)
276 .expect("writing to a Vec cannot fail");
277 let text = String::from_utf8_lossy(&out);
278 assert!(!text.contains("a=f"), "no animation frames");
279 assert!(text.contains("a=t"), "patch transmits carry the surface");
280 }
281
282 #[test]
283 fn an_animation_capable_terminal_gets_frames() {
284 let mut animated_caps = caps(true);
285 animated_caps.animation = true;
286 let animated = crate::surface::AnimatedKitty::new(90, 20, 8, 16);
287 let fallback = crate::surface::PatchedKitty::new(
288 90,
289 CellPixels {
290 width: 2,
291 height: 4,
292 },
293 );
294 let mut screen = PixelScreen::animated(&animated_caps, CELL, animated, fallback);
295 let mut out = Vec::new();
296 screen
297 .render_frame(&mut out, &plain(8, 8), 4, 2)
298 .expect("writing to a Vec cannot fail");
299 let text = String::from_utf8_lossy(&out);
300 assert!(text.contains("a=a"), "playback is terminal-driven");
301 }
302}