1use crate::Sink;
12use crate::anchor::Anchor;
13use std::collections::HashSet;
14use std::io::{self, Write as _};
15
16use katui_proto::kitty::{self, IdSpace, ImageId as KittyImage, PlacementId as KittyPlacement};
17use katui_proto::probe::Capabilities;
18use katui_scene::cell::Position;
19use katui_scene::placement::{ImageId, Placement, PlacementDelta, PlacementId, diff};
20use katui_scene::surface::PixelRect;
21
22const MULTIPLEXER_BAND_BYTES: usize = 12 * 1024;
25
26#[derive(Debug, Clone, Copy)]
29pub struct RgbaImage<'a> {
30 width: u32,
31 height: u32,
32 pixels: &'a [u8],
33}
34
35impl<'a> RgbaImage<'a> {
36 #[must_use]
39 pub fn new(width: u32, height: u32, pixels: &'a [u8]) -> Option<Self> {
40 let expected = (4 * width).saturating_mul(height);
41 (usize::try_from(expected).is_ok_and(|expected| pixels.len() == expected)).then_some(Self {
42 width,
43 height,
44 pixels,
45 })
46 }
47}
48
49#[derive(Debug, Default)]
51pub struct Images<'a> {
52 map: std::collections::HashMap<ImageId, RgbaImage<'a>>,
53}
54
55impl<'a> Images<'a> {
56 pub fn insert(&mut self, id: ImageId, image: RgbaImage<'a>) {
58 self.map.insert(id, image);
59 }
60
61 fn get(&self, id: ImageId) -> Option<RgbaImage<'_>> {
62 self.map.get(&id).copied()
63 }
64}
65
66#[derive(Debug, Default)]
70pub struct Kitty {
71 sent: HashSet<ImageId>,
72 live: HashSet<(ImageId, PlacementId)>,
73 anchor: Option<Anchor>,
74 space: Option<IdSpace>,
75 banded: bool,
76}
77
78impl Kitty {
79 #[must_use]
81 pub fn new() -> Self {
82 Self::default()
83 }
84
85 #[must_use]
93 pub fn for_multiplexer(mut self, space: IdSpace) -> Self {
94 self.space = Some(space);
95 self.anchor = Some(Anchor::new(space));
96 self
97 }
98
99 #[must_use]
103 pub fn banded(mut self, banded: bool) -> Self {
104 self.banded = banded;
105 self
106 }
107
108 fn wire(&self, image: ImageId) -> KittyImage {
111 self.space
112 .map_or(KittyImage(image.0), |space| space.image(image.0))
113 }
114
115 pub fn reset<W: Sink>(&mut self, out: &mut W) -> io::Result<()> {
118 let mut sent: Vec<ImageId> = self.sent.drain().collect();
119 sent.sort_unstable();
120 self.live.clear();
121 if let Some(anchor) = self.anchor.as_mut() {
122 anchor.forget();
123 }
124 for image in sent {
125 out.write_all(&kitty::Delete::Free(self.wire(image)).encode())?;
126 }
127 Ok(())
128 }
129
130 fn upload(
131 &self,
132 bytes: &mut Vec<u8>,
133 image: ImageId,
134 payload: &RgbaImage<'_>,
135 ) -> io::Result<()> {
136 let band = if self.banded {
137 MULTIPLEXER_BAND_BYTES
138 } else {
139 usize::MAX
140 };
141 for frame in kitty::banded_upload(
142 self.wire(image),
143 payload.width,
144 payload.height,
145 payload.pixels,
146 band,
147 )? {
148 bytes.write_all(&frame)?;
149 }
150 Ok(())
151 }
152
153 pub fn render<W: Sink>(
165 &mut self,
166 out: &mut W,
167 delta: &PlacementDelta,
168 images: &Images<'_>,
169 ) -> io::Result<usize> {
170 if delta.is_empty() {
171 return Ok(0);
172 }
173 let mut bytes = Vec::new();
174 match self.anchor.as_mut() {
175 Some(anchor) => anchor.write(&mut bytes)?,
176 None => bytes.write_all(b"\x1b[?2026h")?,
177 }
178 let transmitted = self.place_added(&mut bytes, &delta.added, images)?;
179 self.place_moved(&mut bytes, &delta.moved)?;
180 self.delete_removed(&mut bytes, &delta.removed)?;
181 self.free_emptied(&mut bytes)?;
182 self.sent.extend(transmitted);
183 if self.anchor.is_none() {
184 bytes.write_all(b"\x1b[?2026l")?;
185 }
186 let written = bytes.len();
187 out.write_all(&bytes)?;
188 Ok(written)
189 }
190
191 fn place_added(
194 &mut self,
195 bytes: &mut Vec<u8>,
196 added: &[Placement],
197 images: &Images<'_>,
198 ) -> io::Result<HashSet<ImageId>> {
199 let mut transmitted: HashSet<ImageId> = HashSet::new();
200 for placement in added {
201 let image = placement.image;
202 let on_wire = self.sent.contains(&image) || transmitted.contains(&image);
203 if !on_wire && let Some(payload) = images.get(image) {
204 self.upload(bytes, image, &payload)?;
205 transmitted.insert(image);
206 }
207 if self.sent.contains(&image) || transmitted.contains(&image) {
208 place(bytes, self.wire(image), placement, self.anchor.as_ref())?;
209 self.live.insert((image, placement.id));
210 }
211 }
212 Ok(transmitted)
213 }
214
215 fn place_moved(&self, bytes: &mut Vec<u8>, moved: &[Placement]) -> io::Result<()> {
216 for placement in moved {
217 if self.live.contains(&(placement.image, placement.id)) {
218 place(
219 bytes,
220 self.wire(placement.image),
221 placement,
222 self.anchor.as_ref(),
223 )?;
224 }
225 }
226 Ok(())
227 }
228
229 fn delete_removed(&mut self, bytes: &mut Vec<u8>, removed: &[Placement]) -> io::Result<()> {
230 for placement in removed {
231 self.live.remove(&(placement.image, placement.id));
232 let wire = self.wire(placement.image);
233 bytes.write_all(
234 &kitty::Delete::Placement(wire, KittyPlacement(placement.id.0)).encode(),
235 )?;
236 }
237 Ok(())
238 }
239
240 fn free_emptied(&mut self, bytes: &mut Vec<u8>) -> io::Result<()> {
242 let mut emptied: Vec<ImageId> = self
243 .sent
244 .iter()
245 .copied()
246 .filter(|image| !self.live.iter().any(|(sent, _)| sent == image))
247 .collect();
248 emptied.sort_unstable();
249 for image in emptied {
250 bytes.write_all(&kitty::Delete::Free(self.wire(image)).encode())?;
251 self.sent.remove(&image);
252 }
253 Ok(())
254 }
255}
256
257#[derive(Debug, Default)]
260pub struct KittyScreen {
261 kitty: Kitty,
262 shown: Vec<Placement>,
263 fresh: bool,
264}
265
266impl KittyScreen {
267 #[must_use]
270 pub fn new(caps: &Capabilities) -> Self {
271 let kitty = if caps.multiplexed() {
272 Kitty::new()
273 .for_multiplexer(caps.id_space)
274 .banded(caps.animation)
275 } else {
276 Kitty::new()
277 };
278 Self {
279 kitty,
280 ..Self::default()
281 }
282 }
283
284 pub fn repaint(&mut self) {
287 self.fresh = true;
288 }
289
290 #[must_use]
293 pub fn sends_everything(&self) -> bool {
294 self.fresh || self.shown.is_empty()
295 }
296
297 pub fn present<W: Sink>(
300 &mut self,
301 out: &mut W,
302 next: Vec<Placement>,
303 images: &Images<'_>,
304 ) -> io::Result<usize> {
305 if std::mem::take(&mut self.fresh) {
306 self.kitty.reset(out)?;
307 self.shown.clear();
308 }
309 let delta = diff(&self.shown, &next);
310 let written = self.kitty.render(out, &delta, images)?;
311 self.shown = next;
312 Ok(written)
313 }
314}
315
316pub(crate) fn crop_rect(crop: PixelRect) -> kitty::PixelRect {
318 kitty::PixelRect {
319 x: crop.x,
320 y: crop.y,
321 width: crop.width,
322 height: crop.height,
323 }
324}
325
326fn place(
328 bytes: &mut Vec<u8>,
329 image: KittyImage,
330 placement: &Placement,
331 anchor: Option<&Anchor>,
332) -> io::Result<()> {
333 let origin = Position {
334 x: placement.area.x,
335 y: placement.area.y,
336 };
337 let mut command = kitty::Place::new(image);
338 command.placement = KittyPlacement(placement.id.0);
339 command.columns = u32::from(placement.area.columns);
340 command.rows = u32::from(placement.area.rows);
341 command.crop = placement.crop.map(crop_rect);
342 command.z = placement.z;
343 match anchor {
344 Some(anchor) => command.parent = Some(anchor.offset_to(origin)),
345 None => crate::cells::move_cursor(bytes, origin)?,
346 }
347 bytes.write_all(&command.encode())
348}
349
350#[cfg(test)]
351mod tests {
352 use super::{Images, Kitty, RgbaImage};
353 use katui_proto::kitty::{self, ImageId as KittyImage, PlacementId as KittyPlacement};
354 use katui_scene::placement::{CellRect, ImageId, Placement, PlacementDelta, PlacementId};
355
356 const PIXELS: &[u8] = &[0xff, 0, 0, 0xff];
357
358 fn placement(image: u32, id: u32, x: u16, y: u16) -> Placement {
359 let area = CellRect {
360 x,
361 y,
362 columns: 4,
363 rows: 2,
364 };
365 Placement::new(ImageId(image), PlacementId(id), area)
366 }
367
368 fn delta(added: &[Placement], moved: &[Placement], removed: &[Placement]) -> PlacementDelta {
369 PlacementDelta {
370 added: added.to_vec(),
371 moved: moved.to_vec(),
372 removed: removed.to_vec(),
373 }
374 }
375
376 fn images() -> Images<'static> {
377 let mut images = Images::default();
378 images.insert(
379 ImageId(7),
380 RgbaImage::new(1, 1, PIXELS).expect("the fixture payload is exact"),
381 );
382 images.insert(
383 ImageId(8),
384 RgbaImage::new(1, 1, PIXELS).expect("the fixture payload is exact"),
385 );
386 images
387 }
388
389 fn transmit_frame(id: u32) -> String {
390 let mut transmit = kitty::Transmit::new(KittyImage(id), kitty::Format::Rgba, 1, 1, PIXELS);
391 transmit.compressed = true;
392 let frames = transmit.encode().expect("a Vec encoder cannot fail");
393 let mut joined = String::new();
394 for frame in frames {
395 joined.push_str(&String::from_utf8_lossy(&frame));
396 }
397 joined
398 }
399
400 #[test]
401 fn a_reset_frees_everything_and_sends_it_all_again() {
402 let mut presenter = Kitty::new();
403 let shown = placement(7, 1, 0, 0);
404 let mut out = Vec::new();
405 presenter
406 .render(&mut out, &delta(&[shown], &[], &[]), &images())
407 .expect("writing to a Vec cannot fail");
408 out.clear();
409 presenter
410 .reset(&mut out)
411 .expect("writing to a Vec cannot fail");
412 assert_eq!(out, kitty::Delete::Free(KittyImage(7)).encode());
413 out.clear();
414 presenter
415 .render(&mut out, &delta(&[shown], &[], &[]), &images())
416 .expect("writing to a Vec cannot fail");
417 assert_eq!(occurrences(&String::from_utf8_lossy(&out), "a=t,"), 1);
418 }
419
420 #[test]
421 fn a_shared_screen_bands_big_images_only_where_frames_are_stored() {
422 use super::KittyScreen;
423 use katui_proto::probe::{Capabilities, Identity};
424
425 let big = vec![0x80; 4 * 64 * 64];
426 let send = |animation: bool| {
427 let caps = Capabilities {
428 kitty: true,
429 animation,
430 identity: Some(Identity {
431 name: "tmux".to_owned(),
432 version: None,
433 }),
434 id_space: kitty::IdSpace::of_process(7),
435 ..Capabilities::default()
436 };
437 let mut screen = KittyScreen::new(&caps);
438 let mut images = Images::default();
439 images.insert(
440 ImageId(3),
441 RgbaImage::new(64, 64, &big).expect("exact pixels"),
442 );
443 let mut out = Vec::new();
444 screen
445 .present(&mut out, vec![placement(3, 1, 0, 0)], &images)
446 .expect("writing to a Vec cannot fail");
447 String::from_utf8_lossy(&out).into_owned()
448 };
449 let space = kitty::IdSpace::of_process(7).image(3).0;
450 let banded = send(true);
451 assert!(banded.contains("a=f,"), "frame edits carry the bands");
452 assert!(
453 banded.contains(&format!("i={space},")),
454 "the id sits in our space"
455 );
456 let whole = send(false);
457 assert!(
458 !whole.contains("a=f,"),
459 "a terminal without frames gets one transmit"
460 );
461 assert_eq!(whole.matches("a=t,").count(), 2, "the anchor and the image");
462 }
463
464 #[test]
465 fn a_multiplexer_render_has_no_sync_wrapper_and_keeps_z() {
466 let mut presenter = Kitty::new().for_multiplexer(katui_proto::kitty::IdSpace::default());
467 let layered = Placement {
468 z: -5,
469 ..placement(7, 1, 0, 0)
470 };
471 let mut out = Vec::new();
472 presenter
473 .render(&mut out, &delta(&[layered], &[], &[]), &images())
474 .expect("writing to a Vec cannot fail");
475 let text = String::from_utf8_lossy(&out);
476 assert!(!text.contains("2026"), "queries travel between batches");
477 assert!(text.contains(",z=-5,"), "the layer order reaches the wire");
478 }
479
480 #[test]
481 fn a_screen_sends_only_changes_and_everything_after_a_repaint() {
482 let mut screen = super::KittyScreen::new(&katui_proto::probe::Capabilities::default());
483 let shown = vec![placement(7, 1, 0, 0)];
484 let mut first = Vec::new();
485 screen
486 .present(&mut first, shown.clone(), &images())
487 .expect("writing to a Vec cannot fail");
488 assert!(!screen.sends_everything());
489 let mut same = Vec::new();
490 screen
491 .present(&mut same, shown.clone(), &images())
492 .expect("writing to a Vec cannot fail");
493 assert!(same.is_empty(), "an unchanged frame writes nothing");
494 screen.repaint();
495 assert!(screen.sends_everything());
496 let mut again = Vec::new();
497 screen
498 .present(&mut again, shown, &images())
499 .expect("writing to a Vec cannot fail");
500 let text = String::from_utf8_lossy(&again);
501 assert!(text.contains("a=t,") && text.contains("a=p,"));
502 }
503
504 fn occurrences(text: &str, needle: &str) -> usize {
505 text.matches(needle).count()
506 }
507
508 fn seed(presenter: &mut Kitty, image: u32) {
511 let first = [placement(image, 1, 0, 0)];
512 presenter
513 .render(&mut Vec::new(), &delta(&first, &[], &[]), &images())
514 .expect("writing to a Vec cannot fail");
515 }
516
517 #[test]
518 fn an_empty_diff_writes_nothing() {
519 let mut out = Vec::new();
520 let written = Kitty::new()
521 .render(&mut out, &delta(&[], &[], &[]), &images())
522 .expect("writing to a Vec cannot fail");
523 assert_eq!(written, 0);
524 assert!(out.is_empty());
525 }
526
527 #[test]
528 fn an_exact_payload_is_an_image_and_a_short_one_is_not() {
529 assert!(RgbaImage::new(1, 1, PIXELS).is_some());
530 assert!(RgbaImage::new(2, 1, PIXELS).is_none(), "too few bytes");
531 assert!(
532 RgbaImage::new(0, 0, &[]).is_some(),
533 "an empty image is exact"
534 );
535 }
536
537 #[test]
538 fn an_added_placement_transmits_once_then_places_inside_sync() {
539 let mut out = Vec::new();
540 let added = [placement(7, 1, 2, 3)];
541 Kitty::new()
542 .render(&mut out, &delta(&added, &[], &[]), &images())
543 .expect("writing to a Vec cannot fail");
544 let text = String::from_utf8_lossy(&out);
545 assert!(text.starts_with("\x1b[?2026h"));
546 assert!(text.ends_with("\x1b[?2026l"));
547 let transmit_at = text
548 .find(&transmit_frame(7))
549 .expect("the transmit frame is on the wire");
550 let cursor_at = text.find("\x1b[4;3H").expect("cursor at the origin");
551 let mut place = kitty::Place::new(KittyImage(7));
552 place.placement = KittyPlacement(1);
553 place.columns = 4;
554 place.rows = 2;
555 let place_at = text
556 .find(String::from_utf8_lossy(&place.encode()).as_ref())
557 .expect("the place frame follows");
558 assert!(transmit_at < cursor_at, "payload before addressing");
559 assert!(cursor_at < place_at, "addressing before the place frame");
560 }
561
562 #[test]
563 fn two_pairs_of_one_image_share_one_transmission() {
564 let mut out = Vec::new();
565 let added = [placement(7, 1, 0, 0), placement(7, 2, 6, 0)];
566 let mut presenter = Kitty::new();
567 presenter
568 .render(&mut out, &delta(&added, &[], &[]), &images())
569 .expect("writing to a Vec cannot fail");
570 let text = String::from_utf8_lossy(&out);
571 assert_eq!(
572 occurrences(&text, &transmit_frame(7)),
573 1,
574 "one transmission per image per session"
575 );
576 assert!(text.contains("\x1b[1;1H"));
577 assert!(text.contains("\x1b[1;7H"));
578 }
579
580 #[test]
581 fn a_later_pair_places_without_retransmitting_the_live_image() {
582 let mut presenter = Kitty::new();
583 seed(&mut presenter, 7);
584 let second = [placement(7, 2, 6, 0)];
585 let mut out = Vec::new();
586 presenter
587 .render(&mut out, &delta(&second, &[], &[]), &images())
588 .expect("writing to a Vec cannot fail");
589 let text = String::from_utf8_lossy(&out);
590 assert!(
591 !text.contains("/wAA"),
592 "the live image is never re-transmitted"
593 );
594 assert!(text.contains("\x1b[1;7H"), "the new pair places");
595 }
596
597 #[test]
598 fn a_move_replaces_the_pair_without_transmitting() {
599 let mut presenter = Kitty::new();
600 seed(&mut presenter, 7);
601 let moved = [placement(7, 1, 9, 9)];
602 let mut out = Vec::new();
603 presenter
604 .render(&mut out, &delta(&[], &moved, &[]), &images())
605 .expect("writing to a Vec cannot fail");
606 let text = String::from_utf8_lossy(&out);
607 assert!(!text.contains("/wAA"), "no payload on a move");
608 assert!(text.contains("\x1b[10;10H"));
609 }
610
611 #[test]
612 fn removing_the_last_pair_retires_the_image() {
613 let mut presenter = Kitty::new();
614 seed(&mut presenter, 7);
615 let removed = [placement(7, 1, 0, 0)];
616 let mut out = Vec::new();
617 presenter
618 .render(&mut out, &delta(&[], &[], &removed), &images())
619 .expect("writing to a Vec cannot fail");
620 let text = String::from_utf8_lossy(&out);
621 let pair_delete = kitty::Delete::Placement(KittyImage(7), KittyPlacement(1)).encode();
622 let image_delete = kitty::Delete::Free(KittyImage(7)).encode();
623 let pair_at = text
624 .find(String::from_utf8_lossy(&pair_delete).as_ref())
625 .expect("the pair deletes");
626 let image_at = text
627 .find(String::from_utf8_lossy(&image_delete).as_ref())
628 .expect("the emptied image retires");
629 assert!(pair_at < image_at, "pairs go before their image");
630 }
631
632 #[test]
633 fn an_added_placement_without_pixels_is_skipped() {
634 let mut out = Vec::new();
635 let added = [placement(99, 1, 0, 0)];
636 Kitty::new()
637 .render(&mut out, &delta(&added, &[], &[]), &images())
638 .expect("writing to a Vec cannot fail");
639 assert_eq!(
640 String::from_utf8_lossy(&out),
641 "\x1b[?2026h\x1b[?2026l",
642 "only the sync wrapper"
643 );
644 }
645}