1mod error;
29
30pub use error::*;
31
32#[cfg(windows)]
33mod win;
34#[cfg(windows)]
35pub use win::{query_buffer, query_osc_buffer};
36
37pub fn query<MS: Into<u64>>(query: &str, timeout_ms: MS) -> Result<String, XQError> {
40 const N: usize = 100;
44 let mut response = [0; N];
45 let n = query_buffer(query, &mut response, timeout_ms.into())?;
46 let s = std::str::from_utf8(&response[..n])?;
47 Ok(s.to_string())
48}
49pub fn query_osc<MS: Into<u64>>(query: &str, timeout_ms: MS) -> Result<String, XQError> {
56 const N: usize = 100;
60 let mut response = [0; N];
61 let resp = query_osc_buffer(query, &mut response, timeout_ms.into())?;
62 let s = std::str::from_utf8(resp)?;
63 Ok(s.to_string())
64}
65
66pub(crate) enum OscScan {
73 NeedMore,
75 Found { start: usize, end: usize },
80 NotOsc,
82}
83
84pub(crate) fn scan_osc(buf: &[u8]) -> OscScan {
87 const ESC: u8 = 0x1b;
88 const BEL: u8 = 0x07;
89 let mut osc_start: Option<usize> = None;
90 let mut osc_end: Option<usize> = None;
91 for (i, &b) in buf.iter().enumerate() {
92 match osc_start {
93 None => {
94 if b == ESC {
95 osc_start = Some(i);
96 }
97 }
98 Some(start) => {
99 if osc_end.is_none() && (b == ESC || b == BEL) {
100 osc_end = Some(i);
101 } else if i >= 3
102 && buf[i - 3] == ESC
103 && buf[i - 2] == b'['
104 && buf[i - 1] == b'0'
105 && b == b'n'
106 {
107 return match osc_end {
108 None => OscScan::NotOsc,
109 Some(end) => OscScan::Found {
110 start: start + 1,
111 end,
112 },
113 };
114 }
115 }
116 }
117 }
118 OscScan::NeedMore
119}
120
121#[cfg(unix)]
122use {nix::errno::Errno, std::os::fd::BorrowedFd};
123
124#[cfg(unix)]
129pub fn query_buffer<MS: Into<u64>>(
130 query: &str,
131 buffer: &mut [u8],
132 timeout_ms: MS,
133) -> Result<usize, XQError> {
134 use std::{
135 fs::File,
136 io::{self, Read, Write},
137 os::fd::AsFd,
138 };
139 let stdout = io::stdout();
140 let mut stdout = stdout.lock();
141 write!(stdout, "{}", query)?;
142 stdout.flush()?;
143 let mut stdin = File::open("/dev/tty")?;
144 let stdin_fd = stdin.as_fd();
145 match wait_for_input(stdin_fd, timeout_ms) {
146 Ok(0) => Err(XQError::Timeout),
147 Ok(_) => {
148 let bytes_written = stdin.read(buffer)?;
149 Ok(bytes_written)
150 }
151 Err(e) => Err(XQError::IO(e.into())),
152 }
153}
154
155#[cfg(unix)]
167pub fn query_osc_buffer<'b, MS: Into<u64> + Copy>(
168 query: &str,
169 buffer: &'b mut [u8],
170 timeout_ms: MS,
171) -> Result<&'b [u8], XQError> {
172 use std::{
173 fs::File,
174 io::{self, Read, Write},
175 os::fd::AsFd,
176 };
177 const ESC: char = '\x1b';
178
179 let term = std::env::var("TERM").map_err(|_| XQError::Unsupported)?;
181 if term == "dumb" {
182 return Err(XQError::Unsupported);
183 }
184 let is_screen = term.starts_with("screen");
185
186 let stdout = io::stdout();
187 let mut stdout = stdout.lock();
188
189 if is_screen {
193 write!(stdout, "{ESC}P")?;
194 }
195
196 write!(stdout, "{}", query)?;
197 write!(stdout, "{ESC}[5n")?;
202
203 if is_screen {
204 write!(stdout, "{ESC}\\")?;
205 }
206
207 stdout.flush()?;
208 let mut stdin = File::open("/dev/tty")?;
209 let mut filled = 0;
210 while filled < buffer.len() {
211 let stdin_fd = stdin.as_fd();
212 match wait_for_input(stdin_fd, timeout_ms) {
213 Ok(0) => {
214 return Err(XQError::Timeout);
215 }
216 Ok(_) => {
217 let bytes_read = stdin.read(&mut buffer[filled..])?;
218 if bytes_read == 0 {
219 return Err(XQError::NotAnOSCResponse); }
221 filled += bytes_read;
222 match scan_osc(&buffer[..filled]) {
223 OscScan::Found { start, end } => return Ok(&buffer[start..=end]),
224 OscScan::NotOsc => return Err(XQError::NotAnOSCResponse),
225 OscScan::NeedMore => {}
226 }
227 }
228 Err(e) => {
229 return Err(XQError::IO(e.into()));
230 }
231 }
232 }
233 Err(XQError::BufferOverflow)
234}
235
236#[cfg(not(any(unix, windows)))]
237pub fn query_buffer<MS: Into<u64>>(
238 _query: &str,
239 _buffer: &mut [u8],
240 _timeout_ms: MS,
241) -> Result<usize, XQError> {
242 Err(XQError::Unsupported)
243}
244
245#[cfg(not(any(unix, windows)))]
246pub fn query_osc_buffer<'b, MS: Into<u64> + Copy>(
247 _query: &str,
248 _buffer: &'b mut [u8],
249 _timeout_ms: MS,
250) -> Result<&'b [u8], XQError> {
251 Err(XQError::Unsupported)
252}
253
254#[cfg(all(unix, not(target_os = "macos")))]
255fn wait_for_input<MS: Into<u64>>(fd: BorrowedFd<'_>, timeout_ms: MS) -> Result<i32, Errno> {
256 use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
257
258 let poll_fd = PollFd::new(fd, PollFlags::POLLIN);
259 let timeout = PollTimeout::try_from(timeout_ms.into()).map_err(|_| Errno::EOVERFLOW)?;
260
261 poll(&mut [poll_fd], timeout)
262}
263
264#[cfg(all(unix, target_os = "macos"))]
268fn wait_for_input<MS: Into<u64>>(fd: BorrowedFd<'_>, timeout_ms: MS) -> Result<i32, Errno> {
269 use {
270 nix::sys::{
271 select::{select, FdSet},
272 time::TimeVal,
273 },
274 std::{os::fd::AsRawFd, time::Duration},
275 };
276 let mut fd_set = FdSet::new();
277 fd_set.insert(fd);
278 let dur = Duration::from_millis(timeout_ms.into());
279 let timeout_s = dur.as_secs() as _;
280 let timeout_us = dur.subsec_micros() as _;
281 let mut tv = TimeVal::new(timeout_s, timeout_us);
282
283 select(
284 fd.as_raw_fd() + 1,
285 Some(&mut fd_set),
286 None,
287 None,
288 Some(&mut tv),
289 )
290}
291
292#[cfg(test)]
293mod tests {
294 use super::{scan_osc, OscScan};
295
296 fn found(buf: &[u8]) -> Option<&[u8]> {
297 match scan_osc(buf) {
298 OscScan::Found { start, end } => Some(&buf[start..=end]),
299 _ => None,
300 }
301 }
302
303 #[test]
304 fn bel_terminated_osc_then_fence() {
305 let buf = b"\x1b]11;rgb:1212/3434/5656\x07\x1b[0n";
307 let payload = found(buf).expect("should find OSC payload");
308 assert_eq!(payload, b"]11;rgb:1212/3434/5656\x07");
310 }
311
312 #[test]
313 fn st_terminated_osc_then_fence() {
314 let buf = b"\x1b]11;rgb:1212/3434/5656\x1b\\\x1b[0n";
316 let payload = found(buf).expect("should find OSC payload");
317 assert_eq!(payload, b"]11;rgb:1212/3434/5656\x1b");
318 }
319
320 #[test]
321 fn osc_payload_containing_n_is_found() {
322 let buf = b"\x1b]11;none\x07\x1b[0n";
325 let payload = found(buf).expect("OSC payload with 'n' should still be found");
326 assert_eq!(payload, b"]11;none\x07");
327 }
328
329 #[test]
330 fn fence_answered_first_is_not_osc() {
331 let buf = b"\x1b[0n";
333 assert!(matches!(scan_osc(buf), OscScan::NotOsc));
334 }
335
336 #[test]
337 fn incomplete_needs_more() {
338 let buf = b"\x1b]11;rgb:1212/34";
339 assert!(matches!(scan_osc(buf), OscScan::NeedMore));
340 }
341
342 #[test]
343 fn empty_needs_more() {
344 assert!(matches!(scan_osc(b""), OscScan::NeedMore));
345 }
346}