Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a function for checking keyboard enhancement support #732

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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 @@ -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