Skip to main content

ratatui_image/picker/
cap_parser.rs

1//! Terminal stdio query parser module.
2use std::{fmt::Write, time::Duration};
3
4use crate::picker::{ProtocolType, STDIN_READ_TIMEOUT_MILLIS};
5
6pub struct Parser {
7    data: String,
8    sequence: ResponseParseState,
9}
10
11#[derive(Debug, PartialEq)]
12pub enum ResponseParseState {
13    Unknown,
14    CSIResponse,
15    KittyResponse,
16}
17
18#[derive(Debug, PartialEq, Clone)]
19pub enum Response {
20    Kitty,
21    Sixel,
22    RectangularOps,
23    CellSize(Option<(u16, u16)>),
24    CursorPositionReport(u16, u16),
25    Status,
26}
27
28/// Extra query options
29pub struct QueryStdioOptions {
30    /// Timeout for the stdio query.
31    pub timeout: Duration,
32    /// Query for [Text Sizing Protocol]. The result can be checked by searching for
33    /// [crate::picker::Capability::TextSizingProtocol] in [crate::picker::Picker::capabilities].
34    ///
35    /// [Text Sizing Protocol] <https://sw.kovidgoyal.net/kitty/text-sizing-protocol//>
36    pub text_sizing_protocol: bool,
37    /// Blacklist protocols from the detection query. Currently only kitty can be detected, so that
38    /// is the only ProtocolType that can have any effect here.
39    /// [`crate::picker::Picker`] currently sets ProtocolType::Kitty for WezTerm and Konsole.
40    blacklist_protocols: Vec<ProtocolType>,
41}
42impl QueryStdioOptions {
43    pub(crate) fn blacklist_protocols(&mut self, protocol_types: Vec<ProtocolType>) {
44        self.blacklist_protocols = protocol_types;
45    }
46}
47
48impl Default for QueryStdioOptions {
49    fn default() -> Self {
50        Self {
51            timeout: Duration::from_millis(STDIN_READ_TIMEOUT_MILLIS),
52            text_sizing_protocol: false,
53            blacklist_protocols: Vec::new(),
54        }
55    }
56}
57
58impl Default for Parser {
59    fn default() -> Self {
60        Parser {
61            data: String::new(),
62            sequence: ResponseParseState::Unknown,
63        }
64    }
65}
66
67impl Parser {
68    pub fn new() -> Self {
69        Parser {
70            data: String::new(),
71            sequence: ResponseParseState::Unknown,
72        }
73    }
74    // Tmux requires escapes to be escaped, and some special start/end sequences.
75    pub fn escape_tmux(is_tmux: bool) -> (&'static str, &'static str, &'static str) {
76        match is_tmux {
77            false => ("", "\x1b", ""),
78            true => ("\x1bPtmux;", "\x1b\x1b", "\x1b\\"),
79        }
80    }
81    pub fn query(is_tmux: bool, options: QueryStdioOptions) -> String {
82        let (start, escape, end) = Parser::escape_tmux(is_tmux);
83
84        let mut buf = String::with_capacity(100);
85        buf.push_str(start);
86
87        if !options.blacklist_protocols.contains(&ProtocolType::Kitty) {
88            // Kitty graphics
89            write!(buf, "{escape}_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA{escape}\\").unwrap();
90        }
91
92        if !options.blacklist_protocols.contains(&ProtocolType::Sixel) {
93            // Device Attributes Report 1 (sixel support)
94            write!(buf, "{escape}[c").unwrap();
95        }
96
97        // Font size in pixels
98        write!(buf, "{escape}[16t").unwrap();
99
100        // iTerm2 proprietary, unknown response, untested so far.
101        //write!(buf, "{escape}[1337n").unwrap();
102
103        if options.text_sizing_protocol {
104            const BEL: &str = "\u{7}";
105            // Send CPR (Cursor Position Report) and Text Sizing Protocol commands.
106            // https://sw.kovidgoyal.net/kitty/text-sizing-protocol/#detecting-if-the-terminal-supports-this-protocol
107            // We need to write a CPR, a resized space, and CPR again, to see if it moved the cursor
108            // correctly with extra width.
109            // Do it again for the scaling part of the protocol.
110            // See [Picker::interpret_parser_responses] for how the responses are interpreted - it
111            // differs slightly from the spec!
112            write!(
113                buf,
114                "{escape}[6n{escape}]66;w=2; {BEL}{escape}[6n{escape}]66;s=2; {BEL}{escape}[6n"
115            )
116            .unwrap();
117        }
118
119        // End with Device Status Report, implemented by all terminals, ensure that there is some
120        // response and we don't hang reading forever.
121        write!(buf, "{escape}[5n").unwrap();
122
123        write!(buf, "{end}").unwrap();
124        buf
125    }
126    pub fn push(&mut self, next: char) -> Vec<Response> {
127        match self.sequence {
128            ResponseParseState::Unknown => {
129                match (&self.data[..], next) {
130                    (_, '\x1b') => {
131                        // If the current sequence hasn't been identified yet, start a new one on Esc.
132                        return self.restart();
133                    }
134                    ("_Gi=31", ';') => {
135                        self.sequence = ResponseParseState::KittyResponse;
136                    }
137
138                    ("[", _) => {
139                        self.sequence = ResponseParseState::CSIResponse;
140                    }
141                    _ => {}
142                };
143                self.data.push(next);
144            }
145            ResponseParseState::CSIResponse => {
146                if self.data == "[0" && next == 'n' {
147                    self.restart();
148                    return vec![Response::Status];
149                }
150                match next {
151                    'c' if self.data.starts_with("[?") => {
152                        let mut caps = vec![];
153                        let inner: Vec<&str> = (self.data[2..]).split(';').collect();
154                        for cap in inner {
155                            match cap {
156                                "4" => caps.push(Response::Sixel),
157                                "28" => caps.push(Response::RectangularOps),
158                                _ => {}
159                            }
160                        }
161                        self.restart();
162                        return caps;
163                    }
164                    't' => {
165                        let mut cell_size = None;
166                        let inner: Vec<&str> = self.data.split(';').collect();
167                        if let [_, h, w] = inner[..] {
168                            if let (Ok(h), Ok(w)) = (h.parse::<u16>(), w.parse::<u16>()) {
169                                if w > 0 && h > 0 {
170                                    cell_size = Some((w, h));
171                                }
172                            }
173                        }
174                        self.restart();
175                        return vec![Response::CellSize(cell_size)];
176                    }
177                    'R' => {
178                        let mut cursor_pos = None;
179                        let inner: Vec<&str> = self.data[1..].split(';').collect();
180                        if let [x, w] = inner[..] {
181                            if let (Ok(x), Ok(y)) = (x.parse::<u16>(), w.parse::<u16>()) {
182                                cursor_pos = Some((y, x));
183                            }
184                        }
185                        if let Some((x, y)) = cursor_pos {
186                            self.restart();
187                            return vec![Response::CursorPositionReport(x, y)];
188                        } else {
189                            self.restart();
190                            return vec![];
191                        }
192                    }
193                    '\x1b' => {
194                        // Give up?
195                        return self.restart();
196                    }
197                    _ => {
198                        self.data.push(next);
199                    }
200                };
201            }
202
203            ResponseParseState::KittyResponse => match next {
204                '\\' => {
205                    let caps = match &self.data[..] {
206                        "_Gi=31;OK\x1b" => vec![Response::Kitty],
207                        _ => vec![],
208                    };
209                    self.restart();
210                    return caps;
211                }
212                _ => {
213                    self.data.push(next);
214                }
215            },
216        };
217        vec![]
218    }
219    fn restart(&mut self) -> Vec<Response> {
220        self.data = String::new();
221        self.sequence = ResponseParseState::Unknown;
222        vec![]
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use std::assert_eq;
229
230    use super::{Parser, Response};
231
232    fn parse(response: &str) -> Vec<Response> {
233        let mut parser = Parser::new();
234        let mut caps: Vec<Response> = vec![];
235        for ch in response.chars() {
236            let mut more_caps = parser.push(ch);
237            caps.append(&mut more_caps)
238        }
239        caps
240    }
241
242    #[test]
243    fn test_parse_all() {
244        let caps =
245            parse("\x1b_Gi=31;OK\x1b\\\x1b[?64;4c\x1b[6;7;14t\x1b[6;6R\x1b[7;7R\x1b[6;6R\x1b[0n");
246        assert_eq!(
247            caps,
248            vec![
249                Response::Kitty,
250                Response::Sixel,
251                Response::CellSize(Some((14, 7))),
252                Response::CursorPositionReport(6, 6),
253                Response::CursorPositionReport(7, 7),
254                Response::CursorPositionReport(6, 6),
255                Response::Status,
256            ],
257        );
258    }
259
260    #[test]
261    fn test_parse_only_garbage() {
262        let caps = parse("\x1bhonkey\x1btonkey\x1b[42\x1b\\");
263        assert_eq!(caps, vec![]);
264    }
265
266    #[test]
267    fn test_parse_preceding_garbage() {
268        let caps = parse("\x1bgarbage...\x1b[?64;5c\x1b[0n");
269        assert_eq!(caps, vec![Response::Status]);
270    }
271
272    #[test]
273    fn test_parse_inner_garbage() {
274        let caps = parse("\x1b[6;7;14t\x1bgarbage...\x1b[?64;5c\x1b[0n");
275        assert_eq!(
276            caps,
277            vec![Response::CellSize(Some((14, 7))), Response::Status]
278        );
279    }
280
281    // #[test]
282    // fn test_parse_incomplete_support_in_text_sizing_protocol() {
283    // let caps = parse("\x1b[6;7;14t\x1b[6;6R\x1b[7;7R\x1b[6;6R\x1b[0n");
284    // assert_eq!(
285    // caps,
286    // vec![
287    // Response::CellSize(Some((14, 7))),
288    // Response::CursorPositionReport(6, 6),
289    // Response::CursorPositionReport(7, 7),
290    // Response::CursorPositionReport(6, 6),
291    // Response::Status,
292    // ],
293    // );
294    // }
295}