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(std::iter::repeat_n([255, 0, 0, 255].iter(), 32 * 32).flatten());
35
36        // print the test image with the action set to query
37        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            // check for timeout while polling to avoid blocking the main thread
53            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        // resize image to fit the text height with the Lanczos3 algorithm
96        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        // convert the image to rgba samples
105        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); // image data is base64 encoded
116        let mut image_data = Vec::<u8>::new();
117        for chunk in encoded_image.as_bytes().chunks(4096) {
118            // send a 4096 byte chunk of base64 encoded rgba image data
119            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\\"); // write empty last chunk
131        image_data.extend(format!("\x1B[{}A", image_rows as u32 - 1).as_bytes()); // move cursor to start of image
132        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()); // move cursor to end of image
139
140        Ok(String::from_utf8(image_data)?)
141    }
142}