Repository: crossterm-rs/crossterm (language: rust)

Issue:
Allow checking for keyboard enhancement support
**Is your feature request related to a problem? Please describe.**
The new keyboard enhancement feature is supported by a small (if growing) subset of terminals. It would be nice to know if the current terminal supports the protocol. This allows adjusting expected keyboard inputs, changing shortcuts,  etc.

**Describe the solution you'd like**
The [suggested way](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#detection-of-support-for-this-protocol) to check for support is to:
- send a query escape sequence for the current keyboard enhancement status
- send a query for the primary device attributes
- check if we got two answers, or just one

I'm not immediately sure a) how to reliably do this and b) how we could provide a helper in crossterm itself. Desired workflow would just be something like:

```rust
if crossterm::event::detect_keyboard_enhancement_support(&mut stdout)? {
...
```

**Describe alternatives you've considered if any**
I don't _think_ there's any other way to detect support. Enabling keyboard enhancement then checking the form of incoming events is possible, but that seems messy and unreliable.


Discussion / hints:
@TimonPost , would love your thoughts here.
Would be nice to have this support. I also am not aware with how reliable this is, but we can expose the function at least and have users be able to call it. At least not directly depending crossterm on this logic. 

Fix patch:
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,
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<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 {
@@ -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();
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<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.
 ///
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<bool> {
+    Ok(false)
+}
+
 pub(crate) fn clear(clear_type: ClearType) -> Result<()> {
     let screen_buffer = ScreenBuffer::current()?;
     let csbi = screen_buffer.info()?;


Test patch: