diff --git a/Cargo.toml b/Cargo.toml index 3deb82eb..0da99425 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ proptest = { version = "1.0.0", default-features = false, features = [ "bit-set", "break-dead-code", ] } +rand = "0.9.2" regex = "1.4.2" [[example]] diff --git a/src/kb.rs b/src/kb.rs index 101d77e7..ed5d8cc1 100644 --- a/src/kb.rs +++ b/src/kb.rs @@ -10,6 +10,8 @@ pub enum Key { Unknown, /// Unrecognized sequence containing Esc and a list of chars UnknownEscSeq(Vec), + /// Cursor position (x, y), zero-indexed + CursorPosition(usize, usize), ArrowLeft, ArrowRight, ArrowUp, diff --git a/src/term.rs b/src/term.rs index 2baa9539..02cd7282 100644 --- a/src/term.rs +++ b/src/term.rs @@ -467,6 +467,15 @@ impl Term { move_cursor_right(self, n) } + /// Get the position of the cursor. + /// + /// Returns the current zero-indexed cursor position as a tuple of (x, y). + #[cfg(unix)] + #[inline] + pub fn cursor_position(&self) -> io::Result<(usize, usize)> { + cursor_position() + } + /// Clear the current line. /// /// Position the cursor at the beginning of the current line. diff --git a/src/unix_term.rs b/src/unix_term.rs index 8905f810..fe6b9242 100644 --- a/src/unix_term.rs +++ b/src/unix_term.rs @@ -3,7 +3,7 @@ use core::ptr; use core::{fmt::Display, mem, str}; use std::env; use std::fs; -use std::io::{self, BufRead, BufReader}; +use std::io::{self, BufRead, BufReader, Write}; use std::os::fd::{AsRawFd, RawFd}; #[cfg(not(target_os = "macos"))] @@ -258,6 +258,44 @@ fn read_single_key_impl(fd: RawFd) -> Result { 'H' => Ok(Key::Home), 'F' => Ok(Key::End), 'Z' => Ok(Key::BackTab), + '0'..='9' => { + // This is a special case for handling the response to a cursor + // position request ("\x1b[6n"). The response is given as + // "\x1b[;R", where and are numbers." + let mut buf = String::new(); + buf.push(c2); + while let Some(c) = read_single_char(fd)? { + if c == 'R' { + break; + } else if c.is_ascii_digit() || c == ';' { + buf.push(c); + if buf.len() > 64 { + // Prevent infinite loop in case of malformed input + return Ok(Key::UnknownEscSeq( + buf.chars().collect(), + )); + } + } else { + // If we encounter an unexpected character, we treat it + // as an unknown escape sequence + return Ok(Key::UnknownEscSeq(vec![c1, c2, c])); + } + } + // buf now contains ";" + let v = buf + .split(';') + .map(|s| s.parse::().unwrap_or(0)) + .collect::>(); + if v.len() == 2 { + // x is column, y is row + Ok(Key::CursorPosition( + v[1].saturating_sub(1), + v[0].saturating_sub(1), + )) + } else { + Ok(Key::UnknownEscSeq(buf.chars().collect())) + } + } _ => { let c3 = read_single_char(fd)?; if let Some(c3) = c3 { @@ -335,6 +373,21 @@ fn read_single_key_impl(fd: RawFd) -> Result { } } +pub(crate) fn cursor_position() -> io::Result<(usize, usize)> { + // Send the cursor position request escape sequence + print!("\x1b[6n"); + io::stdout().flush()?; + + // Read the response from the terminal + match read_single_key(false)? { + Key::CursorPosition(x, y) => Ok((x, y)), + _ => Err(io::Error::new( + io::ErrorKind::InvalidData, + "Unexpected response to cursor position request", + )), + } +} + pub(crate) fn read_single_key(ctrlc_key: bool) -> io::Result { let input = Input::unbuffered()?;