From 1f35fcb6a82924ac54d9efbef8274c1d39c1d940 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Fri, 22 Aug 2025 14:04:41 -0600 Subject: [PATCH 1/2] add get cursor position Signed-off-by: Dave Grantham --- Cargo.toml | 7 ++++- examples/cursor_position.rs | 50 ++++++++++++++++++++++++++++++++ src/ansi.rs | 2 +- src/kb.rs | 2 ++ src/term.rs | 8 ++++++ src/unix_term.rs | 57 ++++++++++++++++++++++++++++++++++++- 6 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 examples/cursor_position.rs diff --git a/Cargo.toml b/Cargo.toml index 3deb82eb..d5934405 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "console" description = "A terminal and console abstraction for Rust" -version = "0.16.0" +version = "0.17.0" keywords = ["cli", "terminal", "colors", "console", "ansi"] license = "MIT" edition = "2021" @@ -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]] @@ -56,6 +57,10 @@ required-features = ["std"] name = "cursor_at" required-features = ["std"] +[[example]] +name = "cursor_position" +required-features = ["std"] + [[example]] name = "keyboard" required-features = ["std"] diff --git a/examples/cursor_position.rs b/examples/cursor_position.rs new file mode 100644 index 00000000..b9131610 --- /dev/null +++ b/examples/cursor_position.rs @@ -0,0 +1,50 @@ +extern crate console; +use console::Term; +use std::io; + +fn draw_point(term: &Term, width: usize) -> io::Result<()> { + // no cheating...get the cursor position here + let (x, y) = term.cursor_position()?; + let str = format!("({x}, {y})"); + let w = str.len() + 2; + if x + w > width { + term.move_cursor_left(w - 1)?; + term.write_str(&format!("{str} •"))?; + } else { + term.write_str(&format!("• {str}"))?; + } + Ok(()) +} + +fn main() -> io::Result<()> { + let term = Term::stdout(); + term.hide_cursor()?; + term.clear_screen()?; + + let (height, width): (usize, usize) = (term.size().0 as usize, term.size().1 as usize); + + // draw the four corners + term.move_cursor_to(0, 0)?; + draw_point(&term, width)?; + // this tests the formatting logic + for i in 0..20 { + term.move_cursor_to(width - i - 1, i)?; + draw_point(&term, width)?; + } + term.move_cursor_to(0, height - 2)?; + draw_point(&term, width)?; + term.move_cursor_to(width, height - 2)?; + draw_point(&term, width)?; + + for _ in 0..10 { + let x = rand::random_range(..=width - 1); + let y = rand::random_range(1..=height - 3); + term.move_cursor_to(x, y)?; + draw_point(&term, width)?; + } + + term.move_cursor_to(0, height)?; + term.show_cursor()?; + + Ok(()) +} diff --git a/src/ansi.rs b/src/ansi.rs index a0abb980..d13d16e9 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -189,7 +189,7 @@ fn find_ansi_code_exclusive(it: &mut Peekable) -> Option<(usize, us /// Helper function to strip ansi codes. #[cfg(feature = "alloc")] -pub fn strip_ansi_codes(s: &str) -> Cow { +pub fn strip_ansi_codes(s: &str) -> Cow<'_, str> { let mut char_it = s.char_indices().peekable(); match find_ansi_code_exclusive(&mut char_it) { Some(_) => { 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..75e45520 100644 --- a/src/term.rs +++ b/src/term.rs @@ -467,6 +467,14 @@ 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). + #[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..87d5973c 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,23 @@ 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 + let key = read_single_key(false)?; + + match key { + 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()?; From 33415b6f69933d17ee232736b1c2bb3da4e221d3 Mon Sep 17 00:00:00 2001 From: Dave Grantham Date: Tue, 26 Aug 2025 16:13:43 -0600 Subject: [PATCH 2/2] review changes Signed-off-by: Dave Grantham --- Cargo.toml | 6 +---- examples/cursor_position.rs | 50 ------------------------------------- src/ansi.rs | 2 +- src/term.rs | 1 + src/unix_term.rs | 4 +-- 5 files changed, 4 insertions(+), 59 deletions(-) delete mode 100644 examples/cursor_position.rs diff --git a/Cargo.toml b/Cargo.toml index d5934405..0da99425 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "console" description = "A terminal and console abstraction for Rust" -version = "0.17.0" +version = "0.16.0" keywords = ["cli", "terminal", "colors", "console", "ansi"] license = "MIT" edition = "2021" @@ -57,10 +57,6 @@ required-features = ["std"] name = "cursor_at" required-features = ["std"] -[[example]] -name = "cursor_position" -required-features = ["std"] - [[example]] name = "keyboard" required-features = ["std"] diff --git a/examples/cursor_position.rs b/examples/cursor_position.rs deleted file mode 100644 index b9131610..00000000 --- a/examples/cursor_position.rs +++ /dev/null @@ -1,50 +0,0 @@ -extern crate console; -use console::Term; -use std::io; - -fn draw_point(term: &Term, width: usize) -> io::Result<()> { - // no cheating...get the cursor position here - let (x, y) = term.cursor_position()?; - let str = format!("({x}, {y})"); - let w = str.len() + 2; - if x + w > width { - term.move_cursor_left(w - 1)?; - term.write_str(&format!("{str} •"))?; - } else { - term.write_str(&format!("• {str}"))?; - } - Ok(()) -} - -fn main() -> io::Result<()> { - let term = Term::stdout(); - term.hide_cursor()?; - term.clear_screen()?; - - let (height, width): (usize, usize) = (term.size().0 as usize, term.size().1 as usize); - - // draw the four corners - term.move_cursor_to(0, 0)?; - draw_point(&term, width)?; - // this tests the formatting logic - for i in 0..20 { - term.move_cursor_to(width - i - 1, i)?; - draw_point(&term, width)?; - } - term.move_cursor_to(0, height - 2)?; - draw_point(&term, width)?; - term.move_cursor_to(width, height - 2)?; - draw_point(&term, width)?; - - for _ in 0..10 { - let x = rand::random_range(..=width - 1); - let y = rand::random_range(1..=height - 3); - term.move_cursor_to(x, y)?; - draw_point(&term, width)?; - } - - term.move_cursor_to(0, height)?; - term.show_cursor()?; - - Ok(()) -} diff --git a/src/ansi.rs b/src/ansi.rs index d13d16e9..a0abb980 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -189,7 +189,7 @@ fn find_ansi_code_exclusive(it: &mut Peekable) -> Option<(usize, us /// Helper function to strip ansi codes. #[cfg(feature = "alloc")] -pub fn strip_ansi_codes(s: &str) -> Cow<'_, str> { +pub fn strip_ansi_codes(s: &str) -> Cow { let mut char_it = s.char_indices().peekable(); match find_ansi_code_exclusive(&mut char_it) { Some(_) => { diff --git a/src/term.rs b/src/term.rs index 75e45520..02cd7282 100644 --- a/src/term.rs +++ b/src/term.rs @@ -470,6 +470,7 @@ impl Term { /// 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() diff --git a/src/unix_term.rs b/src/unix_term.rs index 87d5973c..fe6b9242 100644 --- a/src/unix_term.rs +++ b/src/unix_term.rs @@ -379,9 +379,7 @@ pub(crate) fn cursor_position() -> io::Result<(usize, usize)> { io::stdout().flush()?; // Read the response from the terminal - let key = read_single_key(false)?; - - match key { + match read_single_key(false)? { Key::CursorPosition(x, y) => Ok((x, y)), _ => Err(io::Error::new( io::ErrorKind::InvalidData,