Skip to content

Commit

Permalink
Add a function for checking keyboard enhancement support (#732)
Browse files Browse the repository at this point in the history
* 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.

* Check keyboard enhancement in the event-read example
  • Loading branch information
the-mikedavis committed Jan 10, 2023
1 parent bb48b93 commit 3fe13e1
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 9 deletions.
28 changes: 22 additions & 6 deletions examples/event-read.rs
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/event.rs
Expand Up @@ -934,6 +934,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 @@ -177,6 +178,11 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> Result<Option<InternalEvent>> {
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 {
Expand Down Expand Up @@ -251,6 +257,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
70 changes: 70 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,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<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>

// 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.
///
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 3fe13e1

Please sign in to comment.