1use crate::get_dimensions;
2use anyhow::Result;
3use base64::{engine, Engine};
4use image::{imageops::FilterType, DynamicImage};
5use libc::{
6 c_void, poll, pollfd, read, tcgetattr, tcsetattr, termios, ECHO, ICANON, POLLIN, STDIN_FILENO,
7 TCSANOW,
8};
9use std::io::{stdout, Write};
10use std::time::Instant;
11
12pub struct KittyBackend {}
13
14impl KittyBackend {
15 pub fn new() -> Self {
16 Self {}
17 }
18
19 pub fn supported() -> bool {
20 let old_attributes = unsafe {
22 let mut old_attributes: termios = std::mem::zeroed();
23 tcgetattr(STDIN_FILENO, &mut old_attributes);
24
25 let mut new_attributes = old_attributes;
26 new_attributes.c_lflag &= !ICANON;
27 new_attributes.c_lflag &= !ECHO;
28 tcsetattr(STDIN_FILENO, TCSANOW, &new_attributes);
29 old_attributes
30 };
31
32 let mut test_image = Vec::<u8>::with_capacity(32 * 32 * 4);
34 test_image.extend(std::iter::repeat_n([255, 0, 0, 255].iter(), 32 * 32).flatten());
35
36 print!(
38 "\x1B_Gi=1,f=32,s=32,v=32,a=q;{}\x1B\\",
39 engine::general_purpose::STANDARD.encode(&test_image)
40 );
41 stdout().flush().unwrap();
42
43 let start_time = Instant::now();
44 let mut stdin_pollfd = pollfd {
45 fd: STDIN_FILENO,
46 events: POLLIN,
47 revents: 0,
48 };
49 let allowed_bytes = [0x1B, b'_', b'G', b'\\'];
50 let mut buf = Vec::<u8>::new();
51 loop {
52 while unsafe { poll(&mut stdin_pollfd, 1, 0) < 1 } {
54 if start_time.elapsed().as_millis() > 50 {
55 unsafe {
56 tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
57 }
58 return false;
59 }
60 }
61 let mut byte = 0;
62 unsafe {
63 read(STDIN_FILENO, &mut byte as *mut _ as *mut c_void, 1);
64 }
65 if allowed_bytes.contains(&byte) {
66 buf.push(byte);
67 }
68 if buf.starts_with(&[0x1B, b'_', b'G']) && buf.ends_with(&[0x1B, b'\\']) {
69 unsafe {
70 tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
71 }
72 return true;
73 }
74 }
75 }
76}
77
78impl Default for KittyBackend {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl super::ImageBackend for KittyBackend {
85 fn add_image(
86 &self,
87 lines: Vec<String>,
88 image: &DynamicImage,
89 _colors: usize,
90 ) -> Result<String> {
91 let tty_size = unsafe { get_dimensions() };
92 let width_ratio = f64::from(tty_size.ws_col) / f64::from(tty_size.ws_xpixel);
93 let height_ratio = f64::from(tty_size.ws_row) / f64::from(tty_size.ws_ypixel);
94
95 let image = image.resize(
97 u32::MAX,
98 (lines.len() as f64 / height_ratio) as u32,
99 FilterType::Lanczos3,
100 );
101 let _image_columns = width_ratio * f64::from(image.width());
102 let image_rows = height_ratio * f64::from(image.height());
103
104 let rgba_image = image.to_rgba8();
106 let flat_samples = rgba_image.as_flat_samples();
107 let raw_image = flat_samples
108 .image_slice()
109 .expect("Conversion from image to rgba samples failed");
110 assert_eq!(
111 image.width() as usize * image.height() as usize * 4,
112 raw_image.len()
113 );
114
115 let encoded_image = engine::general_purpose::STANDARD.encode(raw_image); let mut image_data = Vec::<u8>::new();
117 for chunk in encoded_image.as_bytes().chunks(4096) {
118 image_data.extend(
120 format!(
121 "\x1B_Gf=32,s={},v={},m=1,a=T;",
122 image.width(),
123 image.height()
124 )
125 .as_bytes(),
126 );
127 image_data.extend(chunk);
128 image_data.extend(b"\x1B\\");
129 }
130 image_data.extend(b"\x1B_Gm=0;\x1B\\"); image_data.extend(format!("\x1B[{}A", image_rows as u32 - 1).as_bytes()); let mut i = 0;
133 for line in &lines {
134 image_data.extend(format!("\x1B[s{line}\x1B[u\x1B[1B").as_bytes());
135 i += 1;
136 }
137 image_data
138 .extend(format!("\n\x1B[{}B", lines.len().max(image_rows as usize) - i).as_bytes()); Ok(String::from_utf8(image_data)?)
141 }
142}