From c5bbcd8d13b52874c2870cfb5001e753e881ab7d Mon Sep 17 00:00:00 2001 From: Charlie Groves Date: Thu, 28 Jul 2022 14:07:43 -0400 Subject: [PATCH] Add bracketed paste parsing --- Cargo.toml | 1 + examples/event-match-modifiers.rs | 2 +- examples/event-read.rs | 41 +++++++++++++---------- src/event.rs | 55 +++++++++++++++++++++++++++++-- src/event/sys/unix/parse.rs | 20 +++++++++++ 5 files changed, 98 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0893b0304..d6330e5b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ all-features = true # [features] default = [] +event-copy = [] event-stream = ["futures-core"] # diff --git a/examples/event-match-modifiers.rs b/examples/event-match-modifiers.rs index 183109dea..c3f75e95d 100644 --- a/examples/event-match-modifiers.rs +++ b/examples/event-match-modifiers.rs @@ -2,7 +2,7 @@ //! //! cargo run --example event-match-modifiers -use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; fn match_event(read_event: Event) { match read_event { diff --git a/examples/event-read.rs b/examples/event-read.rs index 2a32cefba..fa84c14ba 100644 --- a/examples/event-read.rs +++ b/examples/event-read.rs @@ -8,8 +8,8 @@ use crossterm::event::poll; use crossterm::{ cursor::position, event::{ - read, DisableFocusChange, DisableMouseCapture, EnableFocusChange, EnableMouseCapture, - Event, KeyCode, + read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, Event, KeyCode, }, execute, terminal::{disable_raw_mode, enable_raw_mode}, @@ -34,9 +34,9 @@ fn print_events() -> Result<()> { println!("Cursor position: {:?}\r", position()); } - if let Event::Resize(_, _) = event { - let (original_size, new_size) = flush_resize_events(event); - println!("Resize from: {:?}, to: {:?}", original_size, new_size); + if let Event::Resize(x, y) = event { + let (original_size, new_size) = flush_resize_events((x, y)); + println!("Resize from: {:?}, to: {:?}\r", original_size, new_size); } if event == Event::Key(KeyCode::Esc.into()) { @@ -50,18 +50,15 @@ fn print_events() -> Result<()> { // Resize events can occur in batches. // With a simple loop they can be flushed. // This function will keep the first and last resize event. -fn flush_resize_events(event: Event) -> ((u16, u16), (u16, u16)) { - if let Event::Resize(x, y) = event { - let mut last_resize = (x, y); - while let Ok(true) = poll(Duration::from_millis(50)) { - if let Ok(Event::Resize(x, y)) = read() { - last_resize = (x, y); - } +fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) { + let mut last_resize = first_resize; + while let Ok(true) = poll(Duration::from_millis(50)) { + if let Ok(Event::Resize(x, y)) = read() { + last_resize = (x, y); } - - return ((x, y), last_resize); } - ((0, 0), (0, 0)) + + return (first_resize, last_resize); } fn main() -> Result<()> { @@ -70,13 +67,23 @@ fn main() -> Result<()> { enable_raw_mode()?; let mut stdout = stdout(); - execute!(stdout, EnableFocusChange, EnableMouseCapture)?; + execute!( + stdout, + EnableBracketedPaste, + EnableFocusChange, + EnableMouseCapture + )?; if let Err(e) = print_events() { println!("Error: {:?}\r", e); } - execute!(stdout, DisableFocusChange, DisableMouseCapture)?; + execute!( + stdout, + DisableBracketedPaste, + DisableFocusChange, + DisableMouseCapture + )?; disable_raw_mode() } diff --git a/src/event.rs b/src/event.rs index e32ea9515..0dbfac877 100644 --- a/src/event.rs +++ b/src/event.rs @@ -38,6 +38,7 @@ //! Event::FocusLost => println!("FocusLost"), //! Event::Key(event) => println!("{:?}", event), //! Event::Mouse(event) => println!("{:?}", event), +//! Event::Paste(data) => println!("{:?}", data), //! Event::Resize(width, height) => println!("New size {}x{}", width, height), //! } //! } @@ -63,6 +64,7 @@ //! Event::FocusLost => println!("FocusLost"), //! Event::Key(event) => println!("{:?}", event), //! Event::Mouse(event) => println!("{:?}", event), +//! Event::Paste(data) => println!("Pasted {:?}", data), //! Event::Resize(width, height) => println!("New size {}x{}", width, height), //! } //! } else { @@ -416,6 +418,8 @@ impl Command for PopKeyboardEnhancementFlags { /// A command that enables focus event emission. /// +/// It should be paired with [`DisableFocusChange`] at the end of execution. +/// /// Focus events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct EnableFocusChange; @@ -433,8 +437,6 @@ impl Command for EnableFocusChange { } /// A command that disables focus event emission. -/// -/// Focus events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DisableFocusChange; @@ -450,9 +452,52 @@ impl Command for DisableFocusChange { } } +/// A command that enables [bracketed paste mode](https://en.wikipedia.org/wiki/Bracketed-paste). +/// +/// It should be paired with [`DisableBracketedPaste`] at the end of execution. +/// +/// This is not supported in older Windows terminals without +/// [virtual terminal sequences](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences). +#[cfg(not(feature = "event-copy"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnableBracketedPaste; + +#[cfg(not(feature = "event-copy"))] +impl Command for EnableBracketedPaste { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?2004h")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> Result<()> { + Err(io::Error::new( + io::ErrorKind::Unsupported, + "Bracketed paste not implemented in the legacy Windows API.", + )) + } +} + +/// A command that disables bracketed paste mode. +#[cfg(not(feature = "event-copy"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisableBracketedPaste; + +#[cfg(not(feature = "event-copy"))] +impl Command for DisableBracketedPaste { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + f.write_str(csi!("?2004l")) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> Result<()> { + Ok(()) + } +} + /// Represents an event. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] +#[cfg_attr(feature = "event-copy", derive(Copy))] +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)] pub enum Event { /// The terminal gained focus FocusGained, @@ -462,6 +507,10 @@ pub enum Event { Key(KeyEvent), /// A single mouse event with additional pressed modifiers. Mouse(MouseEvent), + /// A string that was pasted into the terminal. Only emitted if bracketed paste has been + /// enabled. + #[cfg(not(feature = "event-copy"))] + Paste(String), /// An resize event with new dimensions after resize (columns, rows). /// **Note** that resize events can be occur in batches. Resize(u16, u16), diff --git a/src/event/sys/unix/parse.rs b/src/event/sys/unix/parse.rs index 37d5ecf70..33716432a 100644 --- a/src/event/sys/unix/parse.rs +++ b/src/event/sys/unix/parse.rs @@ -180,6 +180,13 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> Result> { if !(64..=126).contains(&last_byte) { None } else { + #[cfg(feature = "bracketed-paste")] + if buffer.len() >= 6 { + // If the length is 6 by the time we've hit something in the 64-126 range, + // we're in a bracketed paste. Let it keep reading till it hits its end + // delimiter + return parse_csi_bracketed_paste(buffer); + } match buffer[buffer.len() - 1] { b'M' => return parse_csi_rxvt_mouse(buffer), b'~' => return parse_csi_special_key_code(buffer), @@ -650,6 +657,19 @@ fn parse_cb(cb: u8) -> Result<(MouseEventKind, KeyModifiers)> { Ok((kind, modifiers)) } +#[cfg(feature = "bracketed-paste")] +pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> Result> { + // ESC [ 2 0 0 ~ pasted text ESC 2 0 1 ~ + assert!(buffer.starts_with(b"\x1B[200~")); + + if !buffer.ends_with(b"\x1b[201~") { + Ok(None) + } else { + let paste = String::from_utf8_lossy(&buffer[6..buffer.len() - 6]).to_string(); + Ok(Some(InternalEvent::Event(Event::Paste(paste)))) + } +} + pub(crate) fn parse_utf8_char(buffer: &[u8]) -> Result> { match std::str::from_utf8(buffer) { Ok(s) => {