From b4aeaad3c7c17b48e409eaa3a046db0a36bde63a Mon Sep 17 00:00:00 2001 From: bogzbonny Date: Sun, 22 Sep 2024 13:36:46 -0700 Subject: [PATCH] implement cell size pixels CSI and pixel mouse input example --- Cargo.toml | 4 ++ examples/event-stream-tokio-pixels.rs | 78 +++++++++++++++++++++++++++ src/event.rs | 68 +++++++++++++++++++++++ src/event/filter.rs | 11 ++++ src/event/sys/unix/parse.rs | 20 +++++++ src/terminal.rs | 2 +- src/terminal/sys.rs | 2 +- src/terminal/sys/unix.rs | 56 +++++++++++++++++++ 8 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 examples/event-stream-tokio-pixels.rs diff --git a/Cargo.toml b/Cargo.toml index 27d6448e2..20f01e39c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,6 +122,10 @@ required-features = ["event-stream", "events"] name = "event-stream-tokio" required-features = ["event-stream", "events"] +[[example]] +name = "event-stream-tokio-pixels" +required-features = ["event-stream", "events"] + [[example]] name = "event-read-char-line" required-features = ["events"] diff --git a/examples/event-stream-tokio-pixels.rs b/examples/event-stream-tokio-pixels.rs new file mode 100644 index 000000000..7df41d90f --- /dev/null +++ b/examples/event-stream-tokio-pixels.rs @@ -0,0 +1,78 @@ +//! Demonstrates how to read events asynchronously with tokio. +//! +//! cargo run --features="event-stream" --example event-stream-tokio-pixels + +use std::{io::stdout, time::Duration}; + +use futures::{future::FutureExt, select, StreamExt}; +use futures_timer::Delay; + +use crossterm::{ + cursor::position, + event::{DisableMousePixelCapture, EnableMousePixelCapture, Event, EventStream, KeyCode}, + execute, + terminal::{self, cell_size, disable_raw_mode, enable_raw_mode}, +}; + +const HELP: &str = r#"EventStream based on futures_util::Stream with tokio + - Keyboard, mouse and terminal resize events enabled + - Prints "." every second if there's no event + - Hit "c" to print current cursor position + - Hit "s" to print current the cell size in pixels + - Use Esc to quit +"#; + +async fn print_events() { + let mut reader = EventStream::new(); + + loop { + let mut delay = Delay::new(Duration::from_millis(1_000)).fuse(); + let mut event = reader.next().fuse(); + + select! { + _ = delay => { println!(".\r"); }, + maybe_event = event => { + match maybe_event { + Some(Ok(event)) => { + println!("Event::{:?}\r", event); + + if event == Event::Key(KeyCode::Char('c').into()) { + println!("Cursor position: {:?}\r", position()); + } + + if event == Event::Key(KeyCode::Char('s').into()) { + println!("CSI Cell size (pixels): {:?}\r", cell_size()); + + let s = terminal::window_size().unwrap(); + let width = s.width/(s.columns); + let height = s.height/(s.rows); + println!("Window Calculated Cell size (pixels): {}, {}\r", height, width); + } + + if event == Event::Key(KeyCode::Esc.into()) { + break; + } + } + Some(Err(e)) => println!("Error: {:?}\r", e), + None => break, + } + } + }; + } +} + +#[tokio::main] +async fn main() -> std::io::Result<()> { + println!("{}", HELP); + + enable_raw_mode()?; + + let mut stdout = stdout(); + execute!(stdout, EnableMousePixelCapture)?; + + print_events().await; + + execute!(stdout, DisableMousePixelCapture)?; + + disable_raw_mode() +} diff --git a/src/event.rs b/src/event.rs index 4d28dc154..788c28825 100644 --- a/src/event.rs +++ b/src/event.rs @@ -372,6 +372,71 @@ impl Command for DisableMouseCapture { } } +/// Mouse events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html). +#[cfg(feature = "events")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnableMousePixelCapture; + +#[cfg(feature = "events")] +impl Command for EnableMousePixelCapture { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(concat!( + // Normal tracking: Send mouse X & Y on button press and release + csi!("?1000h"), + // Button-event tracking: Report button motion events (dragging) + csi!("?1002h"), + // Any-event tracking: Report all motion events + csi!("?1003h"), + // RXVT mouse mode: Allows mouse coordinates of >223 + csi!("?1015h"), + // SGR mouse mode: Allows mouse coordinates of >223, preferred over RXVT mode + csi!("?1006h"), + // SGR-Pixels mouse mode: Allows mouse coordinates in pixels + csi!("?1016h"), + )) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::windows::enable_mouse_capture() + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + false + } +} + +/// A command that disables mouse event capturing. +/// +/// Mouse events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisableMousePixelCapture; + +impl Command for DisableMousePixelCapture { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(concat!( + // The inverse commands of EnableMouseCapture, in reverse order. + csi!("?1016l"), + csi!("?1006l"), + csi!("?1015l"), + csi!("?1003l"), + csi!("?1002l"), + csi!("?1000l"), + )) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> std::io::Result<()> { + sys::windows::disable_mouse_capture() + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + false + } +} + /// A command that enables focus event emission. /// /// It should be paired with [`DisableFocusChange`] at the end of execution. @@ -1177,6 +1242,9 @@ pub(crate) enum InternalEvent { /// A cursor position (`col`, `row`). #[cfg(unix)] CursorPosition(u16, u16), + /// The cell size in pixels (`height`, `width`). + #[cfg(unix)] + CellSizePixels(u16, u16), /// The progressive keyboard enhancement flags enabled by the terminal. #[cfg(unix)] KeyboardEnhancementFlags(KeyboardEnhancementFlags), diff --git a/src/event/filter.rs b/src/event/filter.rs index f78730dcd..6eebf59d2 100644 --- a/src/event/filter.rs +++ b/src/event/filter.rs @@ -17,6 +17,17 @@ impl Filter for CursorPositionFilter { } } +#[cfg(unix)] +#[derive(Debug, Clone)] +pub(crate) struct CellPixelSizeFilter; + +#[cfg(unix)] +impl Filter for CellPixelSizeFilter { + fn eval(&self, event: &InternalEvent) -> bool { + matches!(*event, InternalEvent::CellSizePixels(_, _)) + } +} + #[cfg(unix)] #[derive(Debug, Clone)] pub(crate) struct KeyboardEnhancementFlagsFilter; diff --git a/src/event/sys/unix/parse.rs b/src/event/sys/unix/parse.rs index 2019b5f22..5361cb6ed 100644 --- a/src/event/sys/unix/parse.rs +++ b/src/event/sys/unix/parse.rs @@ -202,6 +202,7 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result> { b'~' => return parse_csi_special_key_code(buffer), b'u' => return parse_csi_u_encoded_key_code(buffer), b'R' => return parse_csi_cursor_position(buffer), + b't' => return parse_csi_cell_size_pixels(buffer), _ => return parse_csi_modifier_key_code(buffer), } } @@ -256,6 +257,25 @@ pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result io::Result> { + // ESC [ 6 ; height ; width t + // height - cell height in pixels + // width - cell width in pixels + assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ + assert!(buffer.ends_with(&[b't'])); + + let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) + .map_err(|_| could_not_parse_event_error())?; + + let mut split = s.split(';'); + + let _ = next_parsed::(&mut split)? - 1; // should be 6 + let height = next_parsed::(&mut split)? - 1; + let width = next_parsed::(&mut split)? - 1; + + Ok(Some(InternalEvent::CellSizePixels(height, width))) +} + fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result> { // ESC [ ? flags u assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); // ESC [ ? diff --git a/src/terminal.rs b/src/terminal.rs index e7406bedd..dac9753bc 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -99,7 +99,7 @@ use crate::{csi, impl_display}; pub(crate) mod sys; #[cfg(feature = "events")] -pub use sys::supports_keyboard_enhancement; +pub use sys::{cell_size, supports_keyboard_enhancement}; /// Tells whether the raw mode is enabled. /// diff --git a/src/terminal/sys.rs b/src/terminal/sys.rs index 9dde47d0e..5a763ca1e 100644 --- a/src/terminal/sys.rs +++ b/src/terminal/sys.rs @@ -2,7 +2,7 @@ #[cfg(unix)] #[cfg(feature = "events")] -pub use self::unix::supports_keyboard_enhancement; +pub use self::unix::{cell_size, supports_keyboard_enhancement}; #[cfg(unix)] pub(crate) use self::unix::{ disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size, window_size, diff --git a/src/terminal/sys/unix.rs b/src/terminal/sys/unix.rs index 7129730a6..4e1789f48 100644 --- a/src/terminal/sys/unix.rs +++ b/src/terminal/sys/unix.rs @@ -178,6 +178,62 @@ fn set_terminal_attr(fd: impl AsFd, termios: &Termios) -> io::Result<()> { Ok(()) } +/// Returns the cell size in pixels (height, width). +/// +/// On unix systems, this function will block and possibly time out while +/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called. +#[cfg(feature = "events")] +pub fn cell_size() -> io::Result<(u16, u16)> { + if is_raw_mode_enabled() { + read_cell_size_raw() + } else { + read_cell_size() + } +} + +#[cfg(feature = "events")] +fn read_cell_size() -> io::Result<(u16, u16)> { + enable_raw_mode()?; + let pos = read_cell_size_raw(); + disable_raw_mode()?; + pos +} + +#[cfg(feature = "events")] +fn read_cell_size_raw() -> io::Result<(u16, u16)> { + // Use `ESC [ 16 t` to and retrieve the cell pixel size. + use { + crate::event::{filter::CellPixelSizeFilter, poll_internal, read_internal, InternalEvent}, + std::{ + io::{self, Error, ErrorKind, Write}, + time::Duration, + }, + }; + + let mut stdout = io::stdout(); + stdout.write_all(b"\x1B[16t")?; + stdout.flush()?; + + loop { + match poll_internal(Some(Duration::from_millis(2000)), &CellPixelSizeFilter) { + Ok(true) => { + if let Ok(InternalEvent::CellSizePixels(height, width)) = + read_internal(&CellPixelSizeFilter) + { + return Ok((height, width)); + } + } + Ok(false) => { + return Err(Error::new( + ErrorKind::Other, + "The cell pixel size could not be read within a normal duration", + )); + } + Err(_) => {} + } + } +} + /// Queries the terminal's support for progressive keyboard enhancement. /// /// On unix systems, this function will block and possibly time out while