Add a function for checking keyboard enhancement support (#732)
* 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
This commit is contained in:
parent
bb48b93b46
commit
3fe13e18d8
@ -13,7 +13,7 @@ use crossterm::{
|
|||||||
read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||||
EnableFocusChange, EnableMouseCapture, Event, KeyCode,
|
EnableFocusChange, EnableMouseCapture, Event, KeyCode,
|
||||||
},
|
},
|
||||||
execute,
|
execute, queue,
|
||||||
terminal::{disable_raw_mode, enable_raw_mode},
|
terminal::{disable_raw_mode, enable_raw_mode},
|
||||||
Result,
|
Result,
|
||||||
};
|
};
|
||||||
@ -69,22 +69,38 @@ fn main() -> Result<()> {
|
|||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
let mut stdout = stdout();
|
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!(
|
execute!(
|
||||||
stdout,
|
stdout,
|
||||||
EnableBracketedPaste,
|
EnableBracketedPaste,
|
||||||
EnableFocusChange,
|
EnableFocusChange,
|
||||||
EnableMouseCapture,
|
EnableMouseCapture,
|
||||||
PushKeyboardEnhancementFlags(
|
|
||||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
|
||||||
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
|
|
||||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
|
||||||
)
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if let Err(e) = print_events() {
|
if let Err(e) = print_events() {
|
||||||
println!("Error: {:?}\r", e);
|
println!("Error: {:?}\r", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if supports_keyboard_enhancement {
|
||||||
|
queue!(stdout, PopKeyboardEnhancementFlags)?;
|
||||||
|
}
|
||||||
|
|
||||||
execute!(
|
execute!(
|
||||||
stdout,
|
stdout,
|
||||||
DisableBracketedPaste,
|
DisableBracketedPaste,
|
||||||
|
@ -934,6 +934,12 @@ pub(crate) enum InternalEvent {
|
|||||||
/// A cursor position (`col`, `row`).
|
/// A cursor position (`col`, `row`).
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
CursorPosition(u16, u16),
|
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)]
|
#[cfg(test)]
|
||||||
|
@ -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)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct EventFilter;
|
pub(crate) struct EventFilter;
|
||||||
|
|
||||||
@ -45,7 +74,8 @@ impl Filter for InternalEventFilter {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent, InternalEventFilter,
|
super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent,
|
||||||
|
InternalEventFilter, KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -54,6 +84,23 @@ mod tests {
|
|||||||
assert!(CursorPositionFilter.eval(&InternalEvent::CursorPosition(0, 0)));
|
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]
|
#[test]
|
||||||
fn test_event_filter_filters_events() {
|
fn test_event_filter_filters_events() {
|
||||||
assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
||||||
|
@ -2,8 +2,9 @@ use std::io;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
event::{
|
event::{
|
||||||
Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, MediaKeyCode,
|
Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
|
||||||
ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind,
|
KeyboardEnhancementFlags, MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent,
|
||||||
|
MouseEventKind,
|
||||||
},
|
},
|
||||||
ErrorKind, Result,
|
ErrorKind, Result,
|
||||||
};
|
};
|
||||||
@ -177,6 +178,11 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> Result<Option<InternalEvent>> {
|
|||||||
b'P' => Some(Event::Key(KeyCode::F(1).into())),
|
b'P' => Some(Event::Key(KeyCode::F(1).into())),
|
||||||
b'Q' => Some(Event::Key(KeyCode::F(2).into())),
|
b'Q' => Some(Event::Key(KeyCode::F(2).into())),
|
||||||
b'S' => Some(Event::Key(KeyCode::F(4).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' => {
|
b'0'..=b'9' => {
|
||||||
// Numbered escape code.
|
// Numbered escape code.
|
||||||
if buffer.len() == 3 {
|
if buffer.len() == 3 {
|
||||||
@ -251,6 +257,51 @@ pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> Result<Option<Internal
|
|||||||
Ok(Some(InternalEvent::CursorPosition(x, y)))
|
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 {
|
fn parse_modifiers(mask: u8) -> KeyModifiers {
|
||||||
let modifier_mask = mask.saturating_sub(1);
|
let modifier_mask = mask.saturating_sub(1);
|
||||||
let mut modifiers = KeyModifiers::empty();
|
let mut modifiers = KeyModifiers::empty();
|
||||||
|
@ -98,6 +98,8 @@ use crate::{csi, impl_display, Result};
|
|||||||
|
|
||||||
pub(crate) mod sys;
|
pub(crate) mod sys;
|
||||||
|
|
||||||
|
pub use sys::supports_keyboard_enhancement;
|
||||||
|
|
||||||
/// Tells whether the raw mode is enabled.
|
/// Tells whether the raw mode is enabled.
|
||||||
///
|
///
|
||||||
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
|
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
//! This module provides platform related functions.
|
//! This module provides platform related functions.
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub use self::unix::supports_keyboard_enhancement;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub(crate) use self::unix::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size};
|
pub(crate) use self::unix::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
pub use self::windows::supports_keyboard_enhancement;
|
||||||
|
#[cfg(windows)]
|
||||||
pub(crate) use self::windows::{
|
pub(crate) use self::windows::{
|
||||||
clear, disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, scroll_down, scroll_up,
|
clear, disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, scroll_down, scroll_up,
|
||||||
set_size, set_window_title, size,
|
set_size, set_window_title, size,
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
//! UNIX related logic for terminal manipulation.
|
//! UNIX related logic for terminal manipulation.
|
||||||
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
use std::os::unix::io::{IntoRawFd, RawFd};
|
use std::os::unix::io::{IntoRawFd, RawFd};
|
||||||
|
use std::time::Duration;
|
||||||
use std::{io, mem, process};
|
use std::{io, mem, process};
|
||||||
|
|
||||||
use libc::{
|
use libc::{
|
||||||
@ -11,7 +13,9 @@ use libc::{
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::event::filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter};
|
||||||
use crate::event::sys::unix::file_descriptor::{tty_fd, FileDesc};
|
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
|
// Some(Termios) -> we're in the raw mode and this is the previous mode
|
||||||
// None -> we're not in the raw mode
|
// None -> we're not in the raw mode
|
||||||
@ -88,6 +92,72 @@ pub(crate) fn disable_raw_mode() -> Result<()> {
|
|||||||
Ok(())
|
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
|
/// execute tput with the given argument and parse
|
||||||
/// the output as a u16.
|
/// the output as a u16.
|
||||||
///
|
///
|
||||||
|
@ -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<()> {
|
pub(crate) fn clear(clear_type: ClearType) -> Result<()> {
|
||||||
let screen_buffer = ScreenBuffer::current()?;
|
let screen_buffer = ScreenBuffer::current()?;
|
||||||
let csbi = screen_buffer.info()?;
|
let csbi = screen_buffer.info()?;
|
||||||
|
Loading…
Reference in New Issue
Block a user