diff --git a/src/event.rs b/src/event.rs index 3e308c1a..883a3019 100644 --- a/src/event.rs +++ b/src/event.rs @@ -54,6 +54,7 @@ //! #[cfg(feature = "bracketed-paste")] //! Event::Paste(data) => println!("{:?}", data), //! Event::Resize(width, height) => println!("New size {}x{}", width, height), +//! Event::ApplicationProgramCommand(command) => println!("New APC {}", command), //! } //! } //! execute!( @@ -100,6 +101,7 @@ //! #[cfg(feature = "bracketed-paste")] //! Event::Paste(data) => println!("Pasted {:?}", data), //! Event::Resize(width, height) => println!("New size {}x{}", width, height), +//! Event::ApplicationProgramCommand(command) => println!("New APC {}", command), //! } //! } else { //! // Timeout expired and no `Event` is available @@ -563,6 +565,9 @@ pub enum Event { /// A resize event with new dimensions after resize (columns, rows). /// **Note** that resize events can occur in batches. Resize(u16, u16), + /// Application Program Command sent by the terminal. + /// Primarily used by the Kitty terminal for graphics commands. + ApplicationProgramCommand(String), } impl Event { @@ -755,6 +760,28 @@ impl Event { _ => None, } } + + /// Returns the string of the command if the event is a APC event, otherwise `None`. + /// + /// This is a convenience method that makes code which only cares about resize events easier to write. + /// + /// # Examples + /// + /// ```no_run + /// use crossterm::event; + /// + /// while let Some(command) = event::read()?.as_apc_event() { + /// // ... + /// } + /// # std::io::Result::Ok(()) + /// ``` + #[inline] + pub fn as_apc_event(&self) -> Option<&str> { + match self { + Event::ApplicationProgramCommand(command) => Some(command), + _ => None, + } + } } /// Represents a mouse event. @@ -1713,35 +1740,66 @@ mod tests { assert_eq!(event.as_key_press_event(), Some(ESC_PRESSED)); assert_eq!(event.as_key_release_event(), None); assert_eq!(event.as_key_repeat_event(), None); + assert_eq!(event.as_mouse_event(), None); assert_eq!(event.as_resize_event(), None); + assert_eq!(event.as_apc_event(), None); let event = Event::Key(ESC_RELEASED); assert_eq!(event.as_key_event(), Some(ESC_RELEASED)); assert_eq!(event.as_key_release_event(), Some(ESC_RELEASED)); assert_eq!(event.as_key_press_event(), None); assert_eq!(event.as_key_repeat_event(), None); + assert_eq!(event.as_mouse_event(), None); assert_eq!(event.as_resize_event(), None); + assert_eq!(event.as_apc_event(), None); let event = Event::Key(ESC_REPEAT); assert_eq!(event.as_key_event(), Some(ESC_REPEAT)); assert_eq!(event.as_key_repeat_event(), Some(ESC_REPEAT)); assert_eq!(event.as_key_press_event(), None); assert_eq!(event.as_key_release_event(), None); + assert_eq!(event.as_mouse_event(), None); assert_eq!(event.as_resize_event(), None); + assert_eq!(event.as_apc_event(), None); let event = Event::Resize(1, 1); assert_eq!(event.as_resize_event(), Some((1, 1))); assert_eq!(event.as_key_event(), None); + assert_eq!(event.as_key_release_event(), None); + assert_eq!(event.as_key_press_event(), None); + assert_eq!(event.as_key_repeat_event(), None); + assert_eq!(event.as_mouse_event(), None); + assert_eq!(event.as_apc_event(), None); let event = Event::Mouse(MOUSE_CLICK); assert_eq!(event.as_mouse_event(), Some(MOUSE_CLICK)); assert_eq!(event.as_key_event(), None); + assert_eq!(event.as_key_release_event(), None); + assert_eq!(event.as_key_press_event(), None); + assert_eq!(event.as_key_repeat_event(), None); + assert_eq!(event.as_resize_event(), None); + assert_eq!(event.as_apc_event(), None); #[cfg(feature = "bracketed-paste")] { let event = Event::Paste("".to_string()); assert_eq!(event.as_paste_event(), Some("")); assert_eq!(event.as_key_event(), None); + assert_eq!(event.as_key_release_event(), None); + assert_eq!(event.as_key_press_event(), None); + assert_eq!(event.as_key_repeat_event(), None); + assert_eq!(event.as_resize_event(), None); + assert_eq!(event.as_mouse_event(), None); + assert_eq!(event.as_apc_event(), None); } + + let event = Event::ApplicationProgramCommand("".to_string()); + assert_eq!(event.as_apc_event(), Some("")); + assert_eq!(event.as_key_event(), None); + assert_eq!(event.as_key_release_event(), None); + assert_eq!(event.as_key_press_event(), None); + assert_eq!(event.as_key_repeat_event(), None); + assert_eq!(event.as_resize_event(), None); + assert_eq!(event.as_mouse_event(), None); } } diff --git a/src/event/sys/unix/parse.rs b/src/event/sys/unix/parse.rs index 29377438..9769a1a0 100644 --- a/src/event/sys/unix/parse.rs +++ b/src/event/sys/unix/parse.rs @@ -1,4 +1,4 @@ -use std::io; +use std::{io, str}; use crate::event::{ Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, KeyboardEnhancementFlags, @@ -75,6 +75,7 @@ pub(crate) fn parse_event( } b'[' => parse_csi(buffer), b'\x1B' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))), + b'_' => parse_apc(buffer), _ => parse_event(&buffer[1..], input_available).map(|event_option| { event_option.map(|event| { if let InternalEvent::Event(Event::Key(key_event)) = event { @@ -134,6 +135,20 @@ fn char_code_to_event(code: KeyCode) -> KeyEvent { KeyEvent::new(code, modifiers) } +pub(crate) fn parse_apc(buffer: &[u8]) -> io::Result> { + assert!(buffer.starts_with(b"\x1B_")); // ESC _ + if !buffer.ends_with(b"\x1B\x5C") || buffer.len() == 2 { + return Ok(None); + } + + // APC strings should contain only ASCII characters + let apc_buf = &buffer[2..buffer.len() - 2]; + let input_str = str::from_utf8(apc_buf).map_err(|_| could_not_parse_event_error())?; + let input_event = Event::ApplicationProgramCommand(input_str.to_string()); + + Ok(Some(InternalEvent::Event(input_event))) +} + pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result> { assert!(buffer.starts_with(b"\x1B[")); // ESC [ @@ -995,6 +1010,14 @@ mod tests { KeyModifiers::SHIFT )))), ); + + // parse_apc + assert_eq!( + parse_event("\x1B_APC; Test\x1B\\".as_bytes(), false).unwrap(), + Some(InternalEvent::Event(Event::ApplicationProgramCommand( + "APC; Test".to_string() + ))) + ) } #[test] @@ -1503,4 +1526,30 @@ mod tests { )))), ); } + + #[test] + fn test_parse_apc() { + assert_eq!( + parse_event(b"\x1B__APC; Test_\x1B\\", false).unwrap(), + Some(InternalEvent::Event(Event::ApplicationProgramCommand( + "_APC; Test_".to_string() + ))) + ); + + assert_eq!(parse_event(b"\x1B_", false).unwrap(), None); + assert_eq!(parse_event(b"\x1B_\x1B", false).unwrap(), None); + assert_eq!( + parse_event(b"\x1B_\x1B\\", false).unwrap(), + Some(InternalEvent::Event(Event::ApplicationProgramCommand( + "".to_string() + ))) + ); + + assert_ne!( + parse_event(b"\x1BAPC; Test_\x1B\\", false).unwrap(), + Some(InternalEvent::Event(Event::ApplicationProgramCommand( + "APC; Test_".to_string() + ))) + ); + } } diff --git a/src/terminal.rs b/src/terminal.rs index 3aebc1f8..09a10d07 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -137,7 +137,7 @@ pub fn size() -> io::Result<(u16, u16)> { sys::size() } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct WindowSize { pub rows: u16, pub columns: u16,