1use 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
28pub struct QueryStdioOptions {
30 pub timeout: Duration,
32 pub text_sizing_protocol: bool,
37 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 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 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 write!(buf, "{escape}[c").unwrap();
95 }
96
97 write!(buf, "{escape}[16t").unwrap();
99
100 if options.text_sizing_protocol {
104 const BEL: &str = "\u{7}";
105 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 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 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 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 }