1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
use crate::get_dimensions;
use anyhow::Result;
use base64::{engine, Engine};
use image::{imageops::FilterType, DynamicImage};
use libc::{
    c_void, poll, pollfd, read, tcgetattr, tcsetattr, termios, ECHO, ICANON, POLLIN, STDIN_FILENO,
    TCSANOW,
};
use std::io::{stdout, Write};
use std::time::Instant;

pub struct KittyBackend {}

impl KittyBackend {
    pub fn new() -> Self {
        Self {}
    }

    pub fn supported() -> bool {
        // save terminal attributes and disable canonical input processing mode
        let old_attributes = unsafe {
            let mut old_attributes: termios = std::mem::zeroed();
            tcgetattr(STDIN_FILENO, &mut old_attributes);

            let mut new_attributes = old_attributes;
            new_attributes.c_lflag &= !ICANON;
            new_attributes.c_lflag &= !ECHO;
            tcsetattr(STDIN_FILENO, TCSANOW, &new_attributes);
            old_attributes
        };

        // generate red rgba test image
        let mut test_image = Vec::<u8>::with_capacity(32 * 32 * 4);
        test_image.extend(
            std::iter::repeat([255, 0, 0, 255].iter())
                .take(32 * 32)
                .flatten(),
        );

        // print the test image with the action set to query
        print!(
            "\x1B_Gi=1,f=32,s=32,v=32,a=q;{}\x1B\\",
            engine::general_purpose::STANDARD.encode(&test_image)
        );
        stdout().flush().unwrap();

        let start_time = Instant::now();
        let mut stdin_pollfd = pollfd {
            fd: STDIN_FILENO,
            events: POLLIN,
            revents: 0,
        };
        let allowed_bytes = [0x1B, b'_', b'G', b'\\'];
        let mut buf = Vec::<u8>::new();
        loop {
            // check for timeout while polling to avoid blocking the main thread
            while unsafe { poll(&mut stdin_pollfd, 1, 0) < 1 } {
                if start_time.elapsed().as_millis() > 50 {
                    unsafe {
                        tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
                    }
                    return false;
                }
            }
            let mut byte = 0;
            unsafe {
                read(STDIN_FILENO, &mut byte as *mut _ as *mut c_void, 1);
            }
            if allowed_bytes.contains(&byte) {
                buf.push(byte);
            }
            if buf.starts_with(&[0x1B, b'_', b'G']) && buf.ends_with(&[0x1B, b'\\']) {
                unsafe {
                    tcsetattr(STDIN_FILENO, TCSANOW, &old_attributes);
                }
                return true;
            }
        }
    }
}

impl Default for KittyBackend {
    fn default() -> Self {
        Self::new()
    }
}

impl super::ImageBackend for KittyBackend {
    fn add_image(
        &self,
        lines: Vec<String>,
        image: &DynamicImage,
        _colors: usize,
    ) -> Result<String> {
        let tty_size = unsafe { get_dimensions() };
        let width_ratio = f64::from(tty_size.ws_col) / f64::from(tty_size.ws_xpixel);
        let height_ratio = f64::from(tty_size.ws_row) / f64::from(tty_size.ws_ypixel);

        // resize image to fit the text height with the Lanczos3 algorithm
        let image = image.resize(
            u32::MAX,
            (lines.len() as f64 / height_ratio) as u32,
            FilterType::Lanczos3,
        );
        let _image_columns = width_ratio * f64::from(image.width());
        let image_rows = height_ratio * f64::from(image.height());

        // convert the image to rgba samples
        let rgba_image = image.to_rgba8();
        let flat_samples = rgba_image.as_flat_samples();
        let raw_image = flat_samples
            .image_slice()
            .expect("Conversion from image to rgba samples failed");
        assert_eq!(
            image.width() as usize * image.height() as usize * 4,
            raw_image.len()
        );

        let encoded_image = engine::general_purpose::STANDARD.encode(raw_image); // image data is base64 encoded
        let mut image_data = Vec::<u8>::new();
        for chunk in encoded_image.as_bytes().chunks(4096) {
            // send a 4096 byte chunk of base64 encoded rgba image data
            image_data.extend(
                format!(
                    "\x1B_Gf=32,s={},v={},m=1,a=T;",
                    image.width(),
                    image.height()
                )
                .as_bytes(),
            );
            image_data.extend(chunk);
            image_data.extend(b"\x1B\\");
        }
        image_data.extend(b"\x1B_Gm=0;\x1B\\"); // write empty last chunk
        image_data.extend(format!("\x1B[{}A", image_rows as u32 - 1).as_bytes()); // move cursor to start of image
        let mut i = 0;
        for line in &lines {
            image_data.extend(format!("\x1B[s{}\x1B[u\x1B[1B", line).as_bytes());
            i += 1;
        }
        image_data
            .extend(format!("\n\x1B[{}B", lines.len().max(image_rows as usize) - i).as_bytes()); // move cursor to end of image

        Ok(String::from_utf8(image_data)?)
    }
}