Skip to main content

ratatui_image/protocol/
kitty.rs

1/// https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders
2use std::fmt::Write;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5
6use crate::{Result, picker::cap_parser::Parser};
7use image::DynamicImage;
8use ratatui::{buffer::Buffer, layout::Rect};
9
10use super::{ProtocolTrait, StatefulProtocolTrait};
11
12#[derive(Default, Clone)]
13struct KittyProtoState {
14    transmitted: Arc<AtomicBool>,
15    transmit_str: Option<String>,
16    id: (u32, String, u16), // Full ID, Formatted color ID, ID extra part for diacritic
17}
18
19impl KittyProtoState {
20    fn new(img: &DynamicImage, id: u32, is_tmux: bool) -> Self {
21        let transmit_str = transmit_virtual(img, id, is_tmux);
22        let [id_extra, id_r, id_g, id_b] = id.to_be_bytes();
23        let id_color = format!("\x1b[38;2;{id_r};{id_g};{id_b}m");
24        let id_extra = u16::from(id_extra);
25        Self {
26            transmitted: Arc::new(AtomicBool::new(false)),
27            transmit_str: Some(transmit_str),
28            id: (id, id_color, id_extra),
29        }
30    }
31
32    // Produce the transmit sequence or None if it has already been produced before.
33    fn make_transmit(&self) -> Option<&str> {
34        let transmitted = self.transmitted.swap(true, Ordering::SeqCst);
35
36        if transmitted {
37            None
38        } else {
39            self.transmit_str.as_deref()
40        }
41    }
42}
43
44// Fixed Kitty protocol (transmits once via AtomicBool).
45#[derive(Clone, Default)]
46pub struct Kitty {
47    proto_state: KittyProtoState,
48    area: Rect,
49}
50
51impl Kitty {
52    /// Create a FixedKitty from an image.
53    pub fn new(image: DynamicImage, area: Rect, id: u32, is_tmux: bool) -> Result<Self> {
54        let proto_state = KittyProtoState::new(&image, id, is_tmux);
55        Ok(Self { proto_state, area })
56    }
57}
58
59impl ProtocolTrait for Kitty {
60    fn render(&self, area: Rect, buf: &mut Buffer) {
61        // Transmit only once. This is why self is mut.
62        let seq = self.proto_state.make_transmit();
63
64        render(area, self.area, buf, &self.proto_state.id, seq);
65    }
66
67    fn area(&self) -> Rect {
68        self.area
69    }
70}
71
72#[derive(Clone)]
73pub struct StatefulKitty {
74    id: (u32, String, u16), // Full ID, Formatted color ID, ID extra part for diacritic
75    rect: Rect,
76    proto_state: KittyProtoState,
77    is_tmux: bool,
78}
79
80impl StatefulKitty {
81    pub fn new(id: u32, is_tmux: bool) -> StatefulKitty {
82        let [id_extra, id_r, id_g, id_b] = id.to_be_bytes();
83        let id_color = format!("\x1b[38;2;{id_r};{id_g};{id_b}m");
84        let id_extra = u16::from(id_extra);
85        StatefulKitty {
86            id: (id, id_color, id_extra),
87            rect: Rect::default(),
88            proto_state: KittyProtoState::default(),
89            is_tmux,
90        }
91    }
92}
93
94impl ProtocolTrait for StatefulKitty {
95    fn render(&self, area: Rect, buf: &mut Buffer) {
96        // Transmit only once. This is why self is mut.
97        let seq = self.proto_state.make_transmit();
98
99        render(area, self.rect, buf, &self.id, seq);
100    }
101
102    fn area(&self) -> Rect {
103        self.rect
104    }
105}
106
107impl StatefulProtocolTrait for StatefulKitty {
108    fn resize_encode(&mut self, img: DynamicImage, area: Rect) -> Result<()> {
109        self.rect = area;
110        // If resized then we must transmit again.
111        self.proto_state = KittyProtoState::new(&img, self.id.0, self.is_tmux);
112        Ok(())
113    }
114}
115
116fn render(
117    area: Rect,
118    rect: Rect,
119    buf: &mut Buffer,
120    (_, id_color, id_extra): &(u32, String, u16),
121    mut seq: Option<&str>,
122) {
123    let full_width = area.width.min(rect.width);
124    let width_usize = usize::from(full_width);
125
126    let estimated_placeholder_row_size = id_color.len() +
127        30 +  // diacritics
128        (width_usize * 4) +
129        30; // restore cursor dance
130    let estimated_transmit_row_size =
131        estimated_placeholder_row_size + if let Some(seq) = seq { seq.len() } else { 0 };
132    let mut symbol = String::with_capacity(estimated_transmit_row_size);
133
134    let row_diacritics: String = std::iter::repeat_n('\u{10EEEE}', width_usize - 1).collect();
135
136    // Restore saved cursor position including color, and now we have to move back to
137    // the end of the area.
138    let right = area.width - 1;
139    let down = area.height - 1;
140    let restore_cursor = format!("\x1b[u\x1b[{right}C\x1b[{down}B");
141
142    // Clamp to effectively 297, the number of placeholders in the Kitty protocol.
143    // Anything beyond would just render the something that's wrong, so skip.
144    let height = area.height.min(rect.height).min(DIACRITICS.len() as u16);
145    for y in 0..height {
146        // Draw each line of unicode placeholders but all into the first cell.
147        // I couldn't work out actually drawing into each cell of the buffer so
148        // that `.set_skip(true)` would be made unnecessary. Maybe some other escape
149        // sequence gets sneaked in somehow.
150        // It could also be made so that each cell starts and ends its own escape sequence
151        // with the image id, but maybe that's worse.
152        symbol.clear();
153        if y == 1 {
154            symbol.shrink_to(estimated_placeholder_row_size);
155        }
156
157        // If not transmitted in previous renders, only transmit once at the
158        // first line.
159        if let Some(seq) = seq.take() {
160            symbol.push_str(seq);
161        }
162
163        // Save cursor position, including fg color which is what we want, and start the unicode
164        // placeholder sequence
165        write!(
166            symbol,
167            "\x1b[s{id_color}\u{10EEEE}{}{}{}",
168            diacritic(y),
169            diacritic(0),
170            diacritic(*id_extra)
171        )
172        .unwrap();
173
174        // Add entire row with positions
175        // Use inherited diacritic values
176        symbol.push_str(&row_diacritics);
177
178        for x in 1..full_width {
179            // Skip or something may overwrite it
180            if let Some(cell) = buf.cell_mut((area.left() + x, area.top() + y)) {
181                cell.set_skip(true);
182            }
183        }
184
185        symbol.push_str(&restore_cursor);
186
187        if let Some(cell) = buf.cell_mut((area.left(), area.top() + y)) {
188            cell.set_symbol(&symbol);
189        }
190    }
191}
192
193/// Create a kitty escape sequence for transmitting and virtual-placement.
194///
195/// The image will be transmitted as RGBA in chunks of 4096 bytes.
196/// A "virtual placement" (U=1) is created so that we can place it using unicode placeholders.
197/// Removing the placements when the unicode placeholder is no longer there is being handled
198/// automatically by kitty.
199fn transmit_virtual(img: &DynamicImage, id: u32, is_tmux: bool) -> String {
200    let (w, h) = (img.width(), img.height());
201    let img_rgba8 = img.to_rgba8();
202    let bytes = img_rgba8.as_raw();
203
204    let (start, escape, end) = Parser::escape_tmux(is_tmux);
205
206    // Max chunk size is 4096 bytes of base64 encoded data
207    const CHARS_PER_CHUNK: usize = 4096;
208    const CHUNK_SIZE: usize = (CHARS_PER_CHUNK / 4) * 3;
209    let chunks = bytes.chunks(CHUNK_SIZE);
210    let chunk_count = chunks.len();
211
212    // rough estimation for the worst-case size of what'll be written into `data` in the following
213    // loop
214    const WORST_CASE_ADDITIONAL_CHUNK_0_LEN: usize = 46;
215    let bytes_written_per_chunk = 11 + CHARS_PER_CHUNK + (escape.len() * 2);
216    let reserve_size =
217        (chunk_count * bytes_written_per_chunk) + WORST_CASE_ADDITIONAL_CHUNK_0_LEN + end.len();
218
219    let mut data = String::with_capacity(reserve_size);
220
221    for (i, chunk) in chunks.enumerate() {
222        data.push_str(start);
223        // tmux seems to only allow a limited amount of data in each passthrough sequence, since
224        // we're already chunking the data for the kitty protocol that's a good enough chunk size to
225        // use for the passthrough chunks too.
226        write!(data, "{escape}_Gq=2,").unwrap();
227
228        if i == 0 {
229            write!(data, "i={id},a=T,U=1,f=32,t=d,s={w},v={h},").unwrap();
230        }
231
232        // m=0 means over
233        let more = u8::from(chunk_count > (i + 1));
234        write!(data, "m={more};").unwrap();
235
236        base64_simd::STANDARD.encode_append(chunk, &mut data);
237
238        write!(data, "{escape}\\").unwrap();
239        data.push_str(end);
240    }
241
242    data
243}
244
245/// From https://sw.kovidgoyal.net/kitty/_downloads/1792bad15b12979994cd6ecc54c967a6/rowcolumn-diacritics.txt
246/// See https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders for further explanation.
247static DIACRITICS: [char; 297] = [
248    '\u{305}',
249    '\u{30D}',
250    '\u{30E}',
251    '\u{310}',
252    '\u{312}',
253    '\u{33D}',
254    '\u{33E}',
255    '\u{33F}',
256    '\u{346}',
257    '\u{34A}',
258    '\u{34B}',
259    '\u{34C}',
260    '\u{350}',
261    '\u{351}',
262    '\u{352}',
263    '\u{357}',
264    '\u{35B}',
265    '\u{363}',
266    '\u{364}',
267    '\u{365}',
268    '\u{366}',
269    '\u{367}',
270    '\u{368}',
271    '\u{369}',
272    '\u{36A}',
273    '\u{36B}',
274    '\u{36C}',
275    '\u{36D}',
276    '\u{36E}',
277    '\u{36F}',
278    '\u{483}',
279    '\u{484}',
280    '\u{485}',
281    '\u{486}',
282    '\u{487}',
283    '\u{592}',
284    '\u{593}',
285    '\u{594}',
286    '\u{595}',
287    '\u{597}',
288    '\u{598}',
289    '\u{599}',
290    '\u{59C}',
291    '\u{59D}',
292    '\u{59E}',
293    '\u{59F}',
294    '\u{5A0}',
295    '\u{5A1}',
296    '\u{5A8}',
297    '\u{5A9}',
298    '\u{5AB}',
299    '\u{5AC}',
300    '\u{5AF}',
301    '\u{5C4}',
302    '\u{610}',
303    '\u{611}',
304    '\u{612}',
305    '\u{613}',
306    '\u{614}',
307    '\u{615}',
308    '\u{616}',
309    '\u{617}',
310    '\u{657}',
311    '\u{658}',
312    '\u{659}',
313    '\u{65A}',
314    '\u{65B}',
315    '\u{65D}',
316    '\u{65E}',
317    '\u{6D6}',
318    '\u{6D7}',
319    '\u{6D8}',
320    '\u{6D9}',
321    '\u{6DA}',
322    '\u{6DB}',
323    '\u{6DC}',
324    '\u{6DF}',
325    '\u{6E0}',
326    '\u{6E1}',
327    '\u{6E2}',
328    '\u{6E4}',
329    '\u{6E7}',
330    '\u{6E8}',
331    '\u{6EB}',
332    '\u{6EC}',
333    '\u{730}',
334    '\u{732}',
335    '\u{733}',
336    '\u{735}',
337    '\u{736}',
338    '\u{73A}',
339    '\u{73D}',
340    '\u{73F}',
341    '\u{740}',
342    '\u{741}',
343    '\u{743}',
344    '\u{745}',
345    '\u{747}',
346    '\u{749}',
347    '\u{74A}',
348    '\u{7EB}',
349    '\u{7EC}',
350    '\u{7ED}',
351    '\u{7EE}',
352    '\u{7EF}',
353    '\u{7F0}',
354    '\u{7F1}',
355    '\u{7F3}',
356    '\u{816}',
357    '\u{817}',
358    '\u{818}',
359    '\u{819}',
360    '\u{81B}',
361    '\u{81C}',
362    '\u{81D}',
363    '\u{81E}',
364    '\u{81F}',
365    '\u{820}',
366    '\u{821}',
367    '\u{822}',
368    '\u{823}',
369    '\u{825}',
370    '\u{826}',
371    '\u{827}',
372    '\u{829}',
373    '\u{82A}',
374    '\u{82B}',
375    '\u{82C}',
376    '\u{82D}',
377    '\u{951}',
378    '\u{953}',
379    '\u{954}',
380    '\u{F82}',
381    '\u{F83}',
382    '\u{F86}',
383    '\u{F87}',
384    '\u{135D}',
385    '\u{135E}',
386    '\u{135F}',
387    '\u{17DD}',
388    '\u{193A}',
389    '\u{1A17}',
390    '\u{1A75}',
391    '\u{1A76}',
392    '\u{1A77}',
393    '\u{1A78}',
394    '\u{1A79}',
395    '\u{1A7A}',
396    '\u{1A7B}',
397    '\u{1A7C}',
398    '\u{1B6B}',
399    '\u{1B6D}',
400    '\u{1B6E}',
401    '\u{1B6F}',
402    '\u{1B70}',
403    '\u{1B71}',
404    '\u{1B72}',
405    '\u{1B73}',
406    '\u{1CD0}',
407    '\u{1CD1}',
408    '\u{1CD2}',
409    '\u{1CDA}',
410    '\u{1CDB}',
411    '\u{1CE0}',
412    '\u{1DC0}',
413    '\u{1DC1}',
414    '\u{1DC3}',
415    '\u{1DC4}',
416    '\u{1DC5}',
417    '\u{1DC6}',
418    '\u{1DC7}',
419    '\u{1DC8}',
420    '\u{1DC9}',
421    '\u{1DCB}',
422    '\u{1DCC}',
423    '\u{1DD1}',
424    '\u{1DD2}',
425    '\u{1DD3}',
426    '\u{1DD4}',
427    '\u{1DD5}',
428    '\u{1DD6}',
429    '\u{1DD7}',
430    '\u{1DD8}',
431    '\u{1DD9}',
432    '\u{1DDA}',
433    '\u{1DDB}',
434    '\u{1DDC}',
435    '\u{1DDD}',
436    '\u{1DDE}',
437    '\u{1DDF}',
438    '\u{1DE0}',
439    '\u{1DE1}',
440    '\u{1DE2}',
441    '\u{1DE3}',
442    '\u{1DE4}',
443    '\u{1DE5}',
444    '\u{1DE6}',
445    '\u{1DFE}',
446    '\u{20D0}',
447    '\u{20D1}',
448    '\u{20D4}',
449    '\u{20D5}',
450    '\u{20D6}',
451    '\u{20D7}',
452    '\u{20DB}',
453    '\u{20DC}',
454    '\u{20E1}',
455    '\u{20E7}',
456    '\u{20E9}',
457    '\u{20F0}',
458    '\u{2CEF}',
459    '\u{2CF0}',
460    '\u{2CF1}',
461    '\u{2DE0}',
462    '\u{2DE1}',
463    '\u{2DE2}',
464    '\u{2DE3}',
465    '\u{2DE4}',
466    '\u{2DE5}',
467    '\u{2DE6}',
468    '\u{2DE7}',
469    '\u{2DE8}',
470    '\u{2DE9}',
471    '\u{2DEA}',
472    '\u{2DEB}',
473    '\u{2DEC}',
474    '\u{2DED}',
475    '\u{2DEE}',
476    '\u{2DEF}',
477    '\u{2DF0}',
478    '\u{2DF1}',
479    '\u{2DF2}',
480    '\u{2DF3}',
481    '\u{2DF4}',
482    '\u{2DF5}',
483    '\u{2DF6}',
484    '\u{2DF7}',
485    '\u{2DF8}',
486    '\u{2DF9}',
487    '\u{2DFA}',
488    '\u{2DFB}',
489    '\u{2DFC}',
490    '\u{2DFD}',
491    '\u{2DFE}',
492    '\u{2DFF}',
493    '\u{A66F}',
494    '\u{A67C}',
495    '\u{A67D}',
496    '\u{A6F0}',
497    '\u{A6F1}',
498    '\u{A8E0}',
499    '\u{A8E1}',
500    '\u{A8E2}',
501    '\u{A8E3}',
502    '\u{A8E4}',
503    '\u{A8E5}',
504    '\u{A8E6}',
505    '\u{A8E7}',
506    '\u{A8E8}',
507    '\u{A8E9}',
508    '\u{A8EA}',
509    '\u{A8EB}',
510    '\u{A8EC}',
511    '\u{A8ED}',
512    '\u{A8EE}',
513    '\u{A8EF}',
514    '\u{A8F0}',
515    '\u{A8F1}',
516    '\u{AAB0}',
517    '\u{AAB2}',
518    '\u{AAB3}',
519    '\u{AAB7}',
520    '\u{AAB8}',
521    '\u{AABE}',
522    '\u{AABF}',
523    '\u{AAC1}',
524    '\u{FE20}',
525    '\u{FE21}',
526    '\u{FE22}',
527    '\u{FE23}',
528    '\u{FE24}',
529    '\u{FE25}',
530    '\u{FE26}',
531    '\u{10A0F}',
532    '\u{10A38}',
533    '\u{1D185}',
534    '\u{1D186}',
535    '\u{1D187}',
536    '\u{1D188}',
537    '\u{1D189}',
538    '\u{1D1AA}',
539    '\u{1D1AB}',
540    '\u{1D1AC}',
541    '\u{1D1AD}',
542    '\u{1D242}',
543    '\u{1D243}',
544    '\u{1D244}',
545];
546
547#[inline]
548fn diacritic(y: u16) -> char {
549    *DIACRITICS
550        .get(usize::from(y))
551        .unwrap_or_else(|| &DIACRITICS[0])
552}