diff --git a/src/event.rs b/src/event.rs index 34644676..a3e33e3a 100644 --- a/src/event.rs +++ b/src/event.rs @@ -947,6 +947,9 @@ pub(crate) enum InternalEvent { /// A cursor position (`col`, `row`). #[cfg(unix)] CursorPosition(u16, u16), + /// Terminal window size in pixels (`width`, `height`). + #[cfg(unix)] + WindowSize(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 f78730dc..4b41605b 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 WindowSizeFilter; + +#[cfg(unix)] +impl Filter for WindowSizeFilter { + fn eval(&self, event: &InternalEvent) -> bool { + matches!(*event, InternalEvent::WindowSize(_, _)) + } +} + #[cfg(unix)] #[derive(Debug, Clone)] pub(crate) struct KeyboardEnhancementFlagsFilter; @@ -76,6 +87,7 @@ mod tests { use super::{ super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent, InternalEventFilter, KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter, + WindowSizeFilter, }; #[test] @@ -84,6 +96,12 @@ mod tests { assert!(CursorPositionFilter.eval(&InternalEvent::CursorPosition(0, 0))); } + #[test] + fn test_window_size_filter_filters_window_size() { + assert!(!WindowSizeFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); + assert!(WindowSizeFilter.eval(&InternalEvent::WindowSize(0, 0))); + } + #[test] fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() { assert!(!KeyboardEnhancementFlagsFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); @@ -105,11 +123,13 @@ mod tests { fn test_event_filter_filters_events() { assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); assert!(!EventFilter.eval(&InternalEvent::CursorPosition(0, 0))); + assert!(!EventFilter.eval(&InternalEvent::WindowSize(0, 0))); } #[test] fn test_event_filter_filters_internal_events() { assert!(InternalEventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); assert!(InternalEventFilter.eval(&InternalEvent::CursorPosition(0, 0))); + assert!(InternalEventFilter.eval(&InternalEvent::WindowSize(0, 0))); } } diff --git a/src/event/sys/unix/parse.rs b/src/event/sys/unix/parse.rs index 2019b5f2..333cf84c 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' if buffer[2] == b'4' => return parse_csi_window_size(buffer), _ => return parse_csi_modifier_key_code(buffer), } } @@ -256,6 +257,24 @@ pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result io::Result> { + // ESC [ 4 ; Ch ; Cw t + // Ch - window height (in pixels) + // Cw - window width (in pixels) + assert!(buffer.starts_with(&[b'\x1B', b'[', b'4', b';'])); // ESC [ 4 ; + assert!(buffer.ends_with(&[b't'])); + + let s = std::str::from_utf8(&buffer[4..buffer.len() - 1]) + .map_err(|_| could_not_parse_event_error())?; + + let mut split = s.split(';'); + + let h = next_parsed::(&mut split)?; + let w = next_parsed::(&mut split)?; + + Ok(Some(InternalEvent::WindowSize(w, h))) +} + fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result> { // ESC [ ? flags u assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); // ESC [ ? @@ -924,6 +943,12 @@ mod tests { Some(InternalEvent::CursorPosition(9, 19)) ); + // parse_csi_window_size + assert_eq!( + parse_event(b"\x1B[4;2014;3840t", false).unwrap(), + Some(InternalEvent::WindowSize(3840, 2014)) + ); + // parse_csi assert_eq!( parse_event(b"\x1B[D", false).unwrap(), @@ -1013,6 +1038,14 @@ mod tests { ); } + #[test] + fn test_parse_csi_window_size() { + assert_eq!( + parse_csi_window_size(b"\x1B[4;2014;3840t").unwrap(), + Some(InternalEvent::WindowSize(3840, 2014)) + ); + } + #[test] fn test_parse_csi() { assert_eq!( diff --git a/src/terminal.rs b/src/terminal.rs index e7406bed..9eeccabd 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -154,6 +154,16 @@ pub fn window_size() -> io::Result { sys::window_size() } +/// Returns the terminal size `(width, height)`. +/// +/// Some terminals do not implement the `ioctl` system call to get the size in pixels, +/// but support it by sending a `CSI 14 t` sequence, this function returns the reported pixel size. +#[cfg(unix)] +#[cfg(feature = "events")] +pub fn window_size_csi(timeout: std::time::Duration) -> io::Result<(u16, u16)> { + sys::read_window_size(timeout) +} + /// Disables line wrapping. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DisableLineWrap; diff --git a/src/terminal/sys.rs b/src/terminal/sys.rs index 9dde47d0..2df21cfe 100644 --- a/src/terminal/sys.rs +++ b/src/terminal/sys.rs @@ -1,5 +1,8 @@ //! This module provides platform related functions. +#[cfg(unix)] +#[cfg(feature = "events")] +pub use self::unix::read_window_size; #[cfg(unix)] #[cfg(feature = "events")] pub use self::unix::supports_keyboard_enhancement; diff --git a/src/terminal/sys/unix.rs b/src/terminal/sys/unix.rs index ed545c5b..b96bcfee 100644 --- a/src/terminal/sys/unix.rs +++ b/src/terminal/sys/unix.rs @@ -34,6 +34,46 @@ impl From for WindowSize { } } +#[cfg(feature = "events")] +pub fn read_window_size(timeout: std::time::Duration) -> io::Result<(u16, u16)> { + if is_raw_mode_enabled() { + read_window_size_raw(timeout) + } else { + enable_raw_mode()?; + let size = read_window_size_raw(timeout); + disable_raw_mode()?; + size + } +} + +#[cfg(feature = "events")] +pub(crate) fn read_window_size_raw(timeout: std::time::Duration) -> io::Result<(u16, u16)> { + use crate::event::{filter::WindowSizeFilter, poll_internal, read_internal, InternalEvent}; + use std::io::Write; + + // Use `ESC [ 14 t` to and retrieve the window size. + let mut stdout = io::stdout(); + stdout.write_all(b"\x1B[14t")?; + stdout.flush()?; + + loop { + match poll_internal(Some(timeout), &WindowSizeFilter) { + Ok(true) => { + if let Ok(InternalEvent::WindowSize(w, h)) = read_internal(&WindowSizeFilter) { + return Ok((w, h)); + } + } + Ok(false) => { + return Err(io::Error::new( + io::ErrorKind::Other, + "Could not read the window size", + )); + } + Err(_) => {} + } + } +} + #[allow(clippy::useless_conversion)] pub(crate) fn window_size() -> io::Result { // http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc