xterm_query/lib.rs
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 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
mod error;
use {nix::errno::Errno, std::os::fd::BorrowedFd};
pub use error::*;
/// Query the xterm interface, assuming the terminal is in raw mode
/// (or we would block waiting for a newline).
pub fn query<MS: Into<u64>>(query: &str, timeout_ms: MS) -> Result<String, XQError> {
// I'll use <const N: usize = 100> when default values for const generics
// are stabilized for enough rustc versions
// See https://github.com/rust-lang/rust/issues/44580
const N: usize = 100;
let mut response = [0; N];
let n = query_buffer(query, &mut response, timeout_ms.into())?;
let s = std::str::from_utf8(&response[..n])?;
Ok(s.to_string())
}
/// Query the xterm interface for an OSC sequence, assuming the terminal is in raw mode
/// (or we would block waiting for a newline).
///
/// The query should be a proper OSC sequence (ie already wrapped, eg "\x1b]11;?\x07")
/// as you want it to be sent to stdout but the answer is only the part after the C0 (ESC)
/// and before the OSC terminator (BEL or ESC).
pub fn query_osc<MS: Into<u64>>(query: &str, timeout_ms: MS) -> Result<String, XQError> {
// I'll use <const N: usize = 100> when default values for const generics
// are stabilized for enough rustc versions
// See https://github.com/rust-lang/rust/issues/44580
const N: usize = 100;
let mut response = [0; N];
let resp = query_osc_buffer(query, &mut response, timeout_ms.into())?;
let s = std::str::from_utf8(resp)?;
Ok(s.to_string())
}
/// Query the xterm interface, assuming the terminal is in raw mode
/// (or we would block waiting for a newline).
///
/// Return the number of bytes read.
#[cfg(unix)]
pub fn query_buffer<MS: Into<u64>>(
query: &str,
buffer: &mut [u8],
timeout_ms: MS,
) -> Result<usize, XQError> {
use std::{
fs::File,
io::{self, Read, Write},
os::fd::AsFd,
};
let stdout = io::stdout();
let mut stdout = stdout.lock();
write!(stdout, "{}", query)?;
stdout.flush()?;
let mut stdin = File::open("/dev/tty")?;
let stdin_fd = stdin.as_fd();
match wait_for_input(stdin_fd, timeout_ms) {
Ok(0) => Err(XQError::Timeout),
Ok(_) => {
let bytes_written = stdin.read(buffer)?;
Ok(bytes_written)
}
Err(e) => Err(XQError::IO(e.into())),
}
}
/// Query the xterm interface for an OSC response, assuming the terminal is in raw mode
/// (or we would block waiting for a newline).
///
/// The provided query should be a proper OSC sequence (ie already wrapped, eg "\x1b]11;?\x07")
///
/// Return a slice of the buffer containing the response. This slice excludes
/// - the response start (ESC) and everything before
/// - the response end (ESC or BEL) and everything after
///
/// OSC sequence:
/// <https://en.wikipedia.org/wiki/ANSI_escape_code#OSC_(Operating_System_Command)_sequences>
#[cfg(unix)]
pub fn query_osc_buffer<'b, MS: Into<u64> + Copy>(
query: &str,
buffer: &'b mut [u8],
timeout_ms: MS,
) -> Result<&'b [u8], XQError> {
use std::{
fs::File,
io::{self, Read, Write},
os::fd::AsFd,
};
let stdout = io::stdout();
let mut stdout = stdout.lock();
write!(stdout, "{}", query)?;
stdout.flush()?;
let mut stdin = File::open("/dev/tty")?;
let mut osc_start_idx = None;
let mut bytes_written = 0;
while bytes_written < buffer.len() {
let stdin_fd = stdin.as_fd();
match wait_for_input(stdin_fd, timeout_ms) {
Ok(0) => {
return Err(XQError::Timeout);
}
Ok(_) => {
let bytes_read = stdin.read(&mut buffer[bytes_written..])?;
if bytes_read == 0 {
return Err(XQError::NotAnOSCResponse); // EOF
}
// the sequence must start with a ESC (27) and end either with a ESC or BEL (7)
for i in bytes_written..bytes_written + bytes_read {
let b = buffer[i];
match osc_start_idx {
None => {
if b == 27 {
osc_start_idx = Some(i);
}
}
Some(start_idx) => {
if b == 27 || b == 7 {
return Ok(&buffer[start_idx + 1..=i]);
}
}
}
}
bytes_written += bytes_read;
}
Err(e) => {
return Err(XQError::IO(e.into()));
}
}
}
Err(XQError::BufferOverflow)
}
#[cfg(not(unix))]
pub fn query_buffer(_query: &str, _buffer: &mut [u8], _timeout_ms: u64) -> Result<usize, XQError> {
Err(XQError::Unsupported)
}
#[cfg(not(target_os = "macos"))]
fn wait_for_input<MS: Into<u64>>(fd: BorrowedFd<'_>, timeout_ms: MS) -> Result<i32, Errno> {
use nix::poll::{poll, PollFd, PollFlags, PollTimeout};
let poll_fd = PollFd::new(fd, PollFlags::POLLIN);
let timeout = PollTimeout::try_from(timeout_ms.into()).map_err(|_| Errno::EOVERFLOW)?;
poll(&mut [poll_fd], timeout)
}
// On MacOS, we need to use the `select` instead of `poll` because it doesn't support poll with tty:
//
// https://github.com/tokio-rs/mio/issues/1377
#[cfg(target_os = "macos")]
fn wait_for_input<MS: Into<u64>>(fd: BorrowedFd<'_>, timeout_ms: MS) -> Result<i32, Errno> {
use {
nix::sys::{
select::{select, FdSet},
time::TimeVal,
},
std::{os::fd::AsRawFd, time::Duration},
};
let mut fd_set = FdSet::new();
fd_set.insert(fd);
let timeout_us = Duration::from_millis(timeout_ms.into())
.as_micros()
.try_into()
.map_err(|_| Errno::EOVERFLOW)?;
let mut tv = TimeVal::new(0, timeout_us);
select(
fd.as_raw_fd() + 1,
Some(&mut fd_set),
None,
None,
Some(&mut tv),
)
}