Skip to content

Commit

Permalink
Add a function for checking keyboard enhancement support
Browse files Browse the repository at this point in the history
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
the response to the flags query, we return the flags. Otherwise, the
terminal must not support progressive keyboard enhancement.
  • Loading branch information
the-mikedavis committed Nov 26, 2022
1 parent b13e8ef commit 50d172b
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 3 deletions.
6 changes: 6 additions & 0 deletions src/event.rs
Expand Up @@ -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)]
Expand Down
49 changes: 48 additions & 1 deletion src/event/filter.rs
Expand Up @@ -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;

Expand Down Expand Up @@ -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]
Expand All @@ -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))));
Expand Down
55 changes: 53 additions & 2 deletions src/event/sys/unix/parse.rs
Expand Up @@ -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,
};
Expand Down Expand Up @@ -170,6 +171,11 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> Result<Option<InternalEvent>> {
b'I' => Some(Event::FocusGained),
b'O' => Some(Event::FocusLost),
b';' => return parse_csi_modifier_key_code(buffer),
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 {
Expand Down Expand Up @@ -244,6 +250,51 @@ pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> Result<Option<Internal
Ok(Some(InternalEvent::CursorPosition(x, y)))
}

fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> Result<Option<InternalEvent>> {
// 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<Option<InternalEvent>> {
// 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 <https://vt100.net/docs/vt510-rm/DA1.html>

Ok(Some(InternalEvent::PrimaryDeviceAttributes))
}

fn parse_modifiers(mask: u8) -> KeyModifiers {
let modifier_mask = mask.saturating_sub(1);
let mut modifiers = KeyModifiers::empty();
Expand Down
2 changes: 2 additions & 0 deletions src/terminal.rs
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions 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,
Expand Down
63 changes: 63 additions & 0 deletions 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::{
Expand All @@ -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
Expand Down Expand Up @@ -88,6 +92,65 @@ 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<bool> {
if is_raw_mode_enabled() {
read_supports_keyboard_enhancement_raw()
} else {
read_supports_keyboard_enhancement_flags()
}
}

fn read_supports_keyboard_enhancement_flags() -> Result<bool> {
enable_raw_mode()?;
let flags = read_supports_keyboard_enhancement_raw();
disable_raw_mode()?;
flags
}

fn read_supports_keyboard_enhancement_raw() -> Result<bool> {
// 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 <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#detection-of-support-for-this-protocol>

let mut stdout = io::stdout();
// ESC [ ? u Query progressive keyboard enhancement flags (kitty protocol).
// ESC [ c Query primary device attributes.
stdout.write_all(b"\x1B[?u\x1B[c")?;
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.
///
Expand Down
7 changes: 7 additions & 0 deletions src/terminal/sys/windows.rs
Expand Up @@ -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<bool> {
Ok(false)
}

pub(crate) fn clear(clear_type: ClearType) -> Result<()> {
let screen_buffer = ScreenBuffer::current()?;
let csbi = screen_buffer.info()?;
Expand Down

0 comments on commit 50d172b

Please sign in to comment.