From 392fc34a78fa43cc698164b2b864b781543e8238 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 26 Nov 2022 13:19:22 -0600 Subject: [PATCH 1/2] Add a function for checking keyboard enhancement support This follows the Kitty documentation's recommended way to check for progressive keyboard enhancement: query the flags and then query the primary device attributes (which is broadly supported). If we receive only the device attributes, the protocol is not supported. --- src/event.rs | 6 ++++ src/event/filter.rs | 49 +++++++++++++++++++++++++- src/event/sys/unix/parse.rs | 55 +++++++++++++++++++++++++++-- src/terminal.rs | 2 ++ src/terminal/sys.rs | 4 +++ src/terminal/sys/unix.rs | 70 +++++++++++++++++++++++++++++++++++++ src/terminal/sys/windows.rs | 7 ++++ 7 files changed, 190 insertions(+), 3 deletions(-) diff --git a/src/event.rs b/src/event.rs index 34fd4dd28..7e28d8bf5 100644 --- a/src/event.rs +++ b/src/event.rs @@ -932,6 +932,12 @@ pub(crate) enum InternalEvent { /// A cursor position (`col`, `row`). #[cfg(unix)] CursorPosition(u16, u16), + /// The progressive keyboard enhancement flags enabled by the terminal. + #[cfg(unix)] + KeyboardEnhancementFlags(KeyboardEnhancementFlags), + /// Attributes and architectural class of the terminal. + #[cfg(unix)] + PrimaryDeviceAttributes, } #[cfg(test)] diff --git a/src/event/filter.rs b/src/event/filter.rs index 2b7e29290..f78730dcd 100644 --- a/src/event/filter.rs +++ b/src/event/filter.rs @@ -17,6 +17,35 @@ impl Filter for CursorPositionFilter { } } +#[cfg(unix)] +#[derive(Debug, Clone)] +pub(crate) struct KeyboardEnhancementFlagsFilter; + +#[cfg(unix)] +impl Filter for KeyboardEnhancementFlagsFilter { + fn eval(&self, event: &InternalEvent) -> bool { + // This filter checks for either a KeyboardEnhancementFlags response or + // a PrimaryDeviceAttributes response. If we receive the PrimaryDeviceAttributes + // response but not KeyboardEnhancementFlags, the terminal does not support + // progressive keyboard enhancement. + matches!( + *event, + InternalEvent::KeyboardEnhancementFlags(_) | InternalEvent::PrimaryDeviceAttributes + ) + } +} + +#[cfg(unix)] +#[derive(Debug, Clone)] +pub(crate) struct PrimaryDeviceAttributesFilter; + +#[cfg(unix)] +impl Filter for PrimaryDeviceAttributesFilter { + fn eval(&self, event: &InternalEvent) -> bool { + matches!(*event, InternalEvent::PrimaryDeviceAttributes) + } +} + #[derive(Debug, Clone)] pub(crate) struct EventFilter; @@ -45,7 +74,8 @@ impl Filter for InternalEventFilter { #[cfg(unix)] mod tests { use super::{ - super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent, InternalEventFilter, + super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent, + InternalEventFilter, KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter, }; #[test] @@ -54,6 +84,23 @@ mod tests { assert!(CursorPositionFilter.eval(&InternalEvent::CursorPosition(0, 0))); } + #[test] + fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() { + assert!(!KeyboardEnhancementFlagsFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); + assert!( + KeyboardEnhancementFlagsFilter.eval(&InternalEvent::KeyboardEnhancementFlags( + crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES + )) + ); + assert!(KeyboardEnhancementFlagsFilter.eval(&InternalEvent::PrimaryDeviceAttributes)); + } + + #[test] + fn test_primary_device_attributes_filter_filters_primary_device_attributes() { + assert!(!PrimaryDeviceAttributesFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); + assert!(PrimaryDeviceAttributesFilter.eval(&InternalEvent::PrimaryDeviceAttributes)); + } + #[test] fn test_event_filter_filters_events() { assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); diff --git a/src/event/sys/unix/parse.rs b/src/event/sys/unix/parse.rs index 0cfbd1a5e..654cee682 100644 --- a/src/event/sys/unix/parse.rs +++ b/src/event/sys/unix/parse.rs @@ -2,8 +2,9 @@ use std::io; use crate::{ event::{ - Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, MediaKeyCode, - ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind, + Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, + KeyboardEnhancementFlags, MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent, + MouseEventKind, }, ErrorKind, Result, }; @@ -177,6 +178,11 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> Result> { b'P' => Some(Event::Key(KeyCode::F(1).into())), b'Q' => Some(Event::Key(KeyCode::F(2).into())), b'S' => Some(Event::Key(KeyCode::F(4).into())), + b'?' => match buffer[buffer.len() - 1] { + b'u' => return parse_csi_keyboard_enhancement_flags(buffer), + b'c' => return parse_csi_primary_device_attributes(buffer), + _ => None, + }, b'0'..=b'9' => { // Numbered escape code. if buffer.len() == 3 { @@ -251,6 +257,51 @@ pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> Result Result> { + // ESC [ ? flags u + assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); // ESC [ ? + assert!(buffer.ends_with(&[b'u'])); + + if buffer.len() < 5 { + return Ok(None); + } + + let bits = buffer[3]; + let mut flags = KeyboardEnhancementFlags::empty(); + + if bits & 1 != 0 { + flags |= KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES; + } + if bits & 2 != 0 { + flags |= KeyboardEnhancementFlags::REPORT_EVENT_TYPES; + } + // *Note*: this is not yet supported by crossterm. + // if bits & 4 != 0 { + // flags |= KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS; + // } + if bits & 8 != 0 { + flags |= KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES; + } + // *Note*: this is not yet supported by crossterm. + // if bits & 16 != 0 { + // flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT; + // } + + Ok(Some(InternalEvent::KeyboardEnhancementFlags(flags))) +} + +fn parse_csi_primary_device_attributes(buffer: &[u8]) -> Result> { + // ESC [ 64 ; attr1 ; attr2 ; ... ; attrn ; c + assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); + assert!(buffer.ends_with(&[b'c'])); + + // This is a stub for parsing the primary device attributes. This response is not + // exposed in the crossterm API so we don't need to parse the individual attributes yet. + // See + + Ok(Some(InternalEvent::PrimaryDeviceAttributes)) +} + fn parse_modifiers(mask: u8) -> KeyModifiers { let modifier_mask = mask.saturating_sub(1); let mut modifiers = KeyModifiers::empty(); diff --git a/src/terminal.rs b/src/terminal.rs index b2d4449de..ecc310308 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -98,6 +98,8 @@ use crate::{csi, impl_display, Result}; pub(crate) mod sys; +pub use sys::supports_keyboard_enhancement; + /// Tells whether the raw mode is enabled. /// /// Please have a look at the [raw mode](./index.html#raw-mode) section. diff --git a/src/terminal/sys.rs b/src/terminal/sys.rs index 0c09fb425..c856c690c 100644 --- a/src/terminal/sys.rs +++ b/src/terminal/sys.rs @@ -1,8 +1,12 @@ //! This module provides platform related functions. +#[cfg(unix)] +pub use self::unix::supports_keyboard_enhancement; #[cfg(unix)] pub(crate) use self::unix::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size}; #[cfg(windows)] +pub use self::windows::supports_keyboard_enhancement; +#[cfg(windows)] pub(crate) use self::windows::{ clear, disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, scroll_down, scroll_up, set_size, set_window_title, size, diff --git a/src/terminal/sys/unix.rs b/src/terminal/sys/unix.rs index e8291dd4d..184a8088c 100644 --- a/src/terminal/sys/unix.rs +++ b/src/terminal/sys/unix.rs @@ -1,7 +1,9 @@ //! UNIX related logic for terminal manipulation. use std::fs::File; +use std::io::Write; use std::os::unix::io::{IntoRawFd, RawFd}; +use std::time::Duration; use std::{io, mem, process}; use libc::{ @@ -11,7 +13,9 @@ use libc::{ use parking_lot::Mutex; use crate::error::Result; +use crate::event::filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter}; use crate::event::sys::unix::file_descriptor::{tty_fd, FileDesc}; +use crate::event::{poll_internal, read_internal, InternalEvent}; // Some(Termios) -> we're in the raw mode and this is the previous mode // None -> we're not in the raw mode @@ -88,6 +92,72 @@ pub(crate) fn disable_raw_mode() -> Result<()> { Ok(()) } +/// Queries the terminal's support for progressive keyboard enhancement. +/// +/// 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. +pub fn supports_keyboard_enhancement() -> Result { + if is_raw_mode_enabled() { + read_supports_keyboard_enhancement_raw() + } else { + read_supports_keyboard_enhancement_flags() + } +} + +fn read_supports_keyboard_enhancement_flags() -> Result { + enable_raw_mode()?; + let flags = read_supports_keyboard_enhancement_raw(); + disable_raw_mode()?; + flags +} + +fn read_supports_keyboard_enhancement_raw() -> Result { + // This is the recommended method for testing support for the keyboard enhancement protocol. + // We send a query for the flags supported by the terminal and then the primary device attributes + // query. If we receive the primary device attributes response but not the keyboard enhancement + // flags, none of the flags are supported. + // + // See + + // ESC [ ? u Query progressive keyboard enhancement flags (kitty protocol). + // ESC [ c Query primary device attributes. + const QUERY: &[u8] = b"\x1B[?u\x1B[c"; + + if let Err(_) = File::open("/dev/tty").and_then(|mut file| { + file.write_all(QUERY)?; + file.flush() + }) { + let mut stdout = io::stdout(); + stdout.write_all(QUERY)?; + stdout.flush()?; + } + + loop { + match poll_internal( + Some(Duration::from_millis(2000)), + &KeyboardEnhancementFlagsFilter, + ) { + Ok(true) => { + match read_internal(&KeyboardEnhancementFlagsFilter) { + Ok(InternalEvent::KeyboardEnhancementFlags(_current_flags)) => { + // Flush the PrimaryDeviceAttributes out of the event queue. + read_internal(&PrimaryDeviceAttributesFilter).ok(); + return Ok(true); + } + _ => return Ok(false), + } + } + Ok(false) => { + return Err(io::Error::new( + io::ErrorKind::Other, + "The keyboard enhancement status could not be read within a normal duration", + )); + } + Err(_) => {} + } + } +} + /// execute tput with the given argument and parse /// the output as a u16. /// diff --git a/src/terminal/sys/windows.rs b/src/terminal/sys/windows.rs index 0e5722774..9ca1a32e5 100644 --- a/src/terminal/sys/windows.rs +++ b/src/terminal/sys/windows.rs @@ -58,6 +58,13 @@ pub(crate) fn size() -> Result<(u16, u16)> { )) } +/// Queries the terminal's support for progressive keyboard enhancement. +/// +/// This always returns `Ok(false)` on Windows. +pub fn supports_keyboard_enhancement() -> Result { + Ok(false) +} + pub(crate) fn clear(clear_type: ClearType) -> Result<()> { let screen_buffer = ScreenBuffer::current()?; let csbi = screen_buffer.info()?; From 357023918cdff891de3c56e28936340e23293689 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 26 Nov 2022 13:56:23 -0600 Subject: [PATCH 2/2] Check keyboard enhancement in the event-read example --- examples/event-read.rs | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/examples/event-read.rs b/examples/event-read.rs index d3103111f..0200ab266 100644 --- a/examples/event-read.rs +++ b/examples/event-read.rs @@ -13,7 +13,7 @@ use crossterm::{ read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableFocusChange, EnableMouseCapture, Event, KeyCode, }, - execute, + execute, queue, terminal::{disable_raw_mode, enable_raw_mode}, Result, }; @@ -69,22 +69,38 @@ fn main() -> Result<()> { enable_raw_mode()?; let mut stdout = stdout(); + + let supports_keyboard_enhancement = matches!( + crossterm::terminal::supports_keyboard_enhancement(), + Ok(true) + ); + + if supports_keyboard_enhancement { + queue!( + stdout, + PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES + | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES + | KeyboardEnhancementFlags::REPORT_EVENT_TYPES + ) + )?; + } + execute!( stdout, EnableBracketedPaste, EnableFocusChange, EnableMouseCapture, - PushKeyboardEnhancementFlags( - KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES - | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES - | KeyboardEnhancementFlags::REPORT_EVENT_TYPES - ) )?; if let Err(e) = print_events() { println!("Error: {:?}\r", e); } + if supports_keyboard_enhancement { + queue!(stdout, PopKeyboardEnhancementFlags)?; + } + execute!( stdout, DisableBracketedPaste,