1use 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), }
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 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#[derive(Clone, Default)]
46pub struct Kitty {
47 proto_state: KittyProtoState,
48 area: Rect,
49}
50
51impl Kitty {
52 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 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), 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 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 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 + (width_usize * 4) +
129 30; 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 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 let height = area.height.min(rect.height).min(DIACRITICS.len() as u16);
145 for y in 0..height {
146 symbol.clear();
153 if y == 1 {
154 symbol.shrink_to(estimated_placeholder_row_size);
155 }
156
157 if let Some(seq) = seq.take() {
160 symbol.push_str(seq);
161 }
162
163 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 symbol.push_str(&row_diacritics);
177
178 for x in 1..full_width {
179 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
193fn 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 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 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 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 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
245static 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}