onefetch_image/
kitty.rs

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        // save terminal attributes and disable canonical input processing mode
21        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        // generate red rgba test image
33        let mut test_image = Vec::<u8>::with_capacity(32 * 32 * 4);
34        test_image.extend(
35            std::iter::repeat([255, 0, 0, 255].iter())
36                .take(32 * 32)
37                .flatten(),
38        );
39
40        // print the test image with the action set to query
41        print!(
42            "\x1B_Gi=1,f=32,s=32,v=32,a=q;{}\x1B\\",
43            engine::general_purpose::STANDARD.encode(&test_image)
44        );
45        stdout().flush().unwrap();
46
47        let start_time = Instant::now();
48        let mut stdin_pollfd = pollfd {
49            fd: STDIN_FILENO,
50            events: POLLIN,
51            revents: 0,
52        };
53        let allowed_bytes = [0x1B, b'_', b'G', b'\\'];
54        let mut buf = Vec::<u8>::new();
55        loop {
56            // check for timeout while polling to avoid blocking the main thread
57            while unsafe { poll(&mut stdin_pollfd, 1, 0) < 1 } {
58                if start_time.elapsed().as_millis() > 50 {
59                    unsafe {
60                        tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
61                    }
62                    return false;
63                }
64            }
65            let mut byte = 0;
66            unsafe {
67                read(STDIN_FILENO, &mut byte as *mut _ as *mut c_void, 1);
68            }
69            if allowed_bytes.contains(&byte) {
70                buf.push(byte);
71            }
72            if buf.starts_with(&[0x1B, b'_', b'G']) && buf.ends_with(&[0x1B, b'\\']) {
73                unsafe {
74                    tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
75                }
76                return true;
77            }
78        }
79    }
80}
81
82impl Default for KittyBackend {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl super::ImageBackend for KittyBackend {
89    fn add_image(
90        &self,
91        lines: Vec<String>,
92        image: &DynamicImage,
93        _colors: usize,
94    ) -> Result<String> {
95        let tty_size = unsafe { get_dimensions() };
96        let width_ratio = f64::from(tty_size.ws_col) / f64::from(tty_size.ws_xpixel);
97        let height_ratio = f64::from(tty_size.ws_row) / f64::from(tty_size.ws_ypixel);
98
99        // resize image to fit the text height with the Lanczos3 algorithm
100        let image = image.resize(
101            u32::MAX,
102            (lines.len() as f64 / height_ratio) as u32,
103            FilterType::Lanczos3,
104        );
105        let _image_columns = width_ratio * f64::from(image.width());
106        let image_rows = height_ratio * f64::from(image.height());
107
108        // convert the image to rgba samples
109        let rgba_image = image.to_rgba8();
110        let flat_samples = rgba_image.as_flat_samples();
111        let raw_image = flat_samples
112            .image_slice()
113            .expect("Conversion from image to rgba samples failed");
114        assert_eq!(
115            image.width() as usize * image.height() as usize * 4,
116            raw_image.len()
117        );
118
119        let encoded_image = engine::general_purpose::STANDARD.encode(raw_image); // image data is base64 encoded
120        let mut image_data = Vec::<u8>::new();
121        for chunk in encoded_image.as_bytes().chunks(4096) {
122            // send a 4096 byte chunk of base64 encoded rgba image data
123            image_data.extend(
124                format!(
125                    "\x1B_Gf=32,s={},v={},m=1,a=T;",
126                    image.width(),
127                    image.height()
128                )
129                .as_bytes(),
130            );
131            image_data.extend(chunk);
132            image_data.extend(b"\x1B\\");
133        }
134        image_data.extend(b"\x1B_Gm=0;\x1B\\"); // write empty last chunk
135        image_data.extend(format!("\x1B[{}A", image_rows as u32 - 1).as_bytes()); // move cursor to start of image
136        let mut i = 0;
137        for line in &lines {
138            image_data.extend(format!("\x1B[s{}\x1B[u\x1B[1B", line).as_bytes());
139            i += 1;
140        }
141        image_data
142            .extend(format!("\n\x1B[{}B", lines.len().max(image_rows as usize) - i).as_bytes()); // move cursor to end of image
143
144        Ok(String::from_utf8(image_data)?)
145    }
146}