diff --git a/.github/workflows/crossterm_test.yml b/.github/workflows/crossterm_test.yml index 3ce9f1fef..8b2509fc2 100644 --- a/.github/workflows/crossterm_test.yml +++ b/.github/workflows/crossterm_test.yml @@ -62,6 +62,9 @@ jobs: - name: Test all features run: cargo test --all-features -- --nocapture --test-threads 1 continue-on-error: ${{ matrix.can-fail }} + - name: Test no default features + run: cargo test --no-default-features -- --nocapture --test-threads 1 + continue-on-error: ${{ matrix.can-fail }} - name: Test Packaging if: matrix.rust == 'stable' run: cargo package diff --git a/Cargo.toml b/Cargo.toml index 0893b0304..197b22a16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,8 @@ all-features = true # Features # [features] -default = [] +default = ["bracketed-paste"] +bracketed-paste = [] event-stream = ["futures-core"] # @@ -72,6 +73,10 @@ serde_json = "1.0" # # Examples # +[[example]] +name = "event-read" +required-features = ["bracketed-paste"] + [[example]] name = "event-stream-async-std" required-features = ["event-stream"] 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 549e05a91..74a9b6b0c 100644 --- a/examples/event-read.rs +++ b/examples/event-read.rs @@ -10,8 +10,8 @@ use crossterm::event::{ 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}, @@ -36,9 +36,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()) { @@ -52,18 +52,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<()> { @@ -74,8 +71,9 @@ fn main() -> Result<()> { let mut stdout = stdout(); execute!( stdout, + EnableBracketedPaste, EnableFocusChange, - EnableMouseCapture, + EnableMouseCapture PushKeyboardEnhancementFlags( KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES @@ -89,6 +87,7 @@ fn main() -> Result<()> { execute!( stdout, + DisableBracketedPaste, PopKeyboardEnhancementFlags, DisableFocusChange, DisableMouseCapture diff --git a/src/event.rs b/src/event.rs index 42411b543..ab8957580 100644 --- a/src/event.rs +++ b/src/event.rs @@ -38,6 +38,8 @@ //! Event::FocusLost => println!("FocusLost"), //! Event::Key(event) => println!("{:?}", event), //! Event::Mouse(event) => println!("{:?}", event), +//! #[cfg(feature = "bracketed-paste")] +//! Event::Paste(data) => println!("{:?}", data), //! Event::Resize(width, height) => println!("New size {}x{}", width, height), //! } //! } @@ -63,6 +65,8 @@ //! Event::FocusLost => println!("FocusLost"), //! Event::Key(event) => println!("{:?}", event), //! Event::Mouse(event) => println!("{:?}", event), +//! #[cfg(feature = "bracketed-paste")] +//! Event::Paste(data) => println!("Pasted {:?}", data), //! Event::Resize(width, height) => println!("New size {}x{}", width, height), //! } //! } else { @@ -416,6 +420,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 +439,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 +454,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(feature = "bracketed-paste")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EnableBracketedPaste; + +#[cfg(feature = "bracketed-paste")] +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(feature = "bracketed-paste")] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DisableBracketedPaste; + +#[cfg(feature = "bracketed-paste")] +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(not(feature = "bracketed-paste"), derive(Copy))] +#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)] pub enum Event { /// The terminal gained focus FocusGained, @@ -462,6 +509,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(feature = "bracketed-paste")] + 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 d96a2f366..e5b245962 100644 --- a/src/event/sys/unix/parse.rs +++ b/src/event/sys/unix/parse.rs @@ -177,11 +177,15 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> Result> { } else { // The final byte of a CSI sequence can be in the range 64-126, so // let's keep reading anything else. - let last_byte = *buffer.last().unwrap(); + let last_byte = buffer[buffer.len() - 1]; if !(64..=126).contains(&last_byte) { None } else { - match buffer[buffer.len() - 1] { + #[cfg(feature = "bracketed-paste")] + if buffer.starts_with(b"\x1B[200~") { + return parse_csi_bracketed_paste(buffer); + } + match last_byte { b'M' => return parse_csi_rxvt_mouse(buffer), b'~' => return parse_csi_special_key_code(buffer), b'u' => return parse_csi_u_encoded_key_code(buffer), @@ -706,6 +710,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) => { @@ -829,6 +846,15 @@ mod tests { Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))), ); + // parse_csi_bracketed_paste + #[cfg(feature = "bracketed-paste")] + assert_eq!( + parse_event(b"\x1B[200~on and on and on\x1B[201~", false).unwrap(), + Some(InternalEvent::Event(Event::Paste( + "on and on and on".to_string() + ))), + ); + // parse_csi_rxvt_mouse assert_eq!( parse_event(b"\x1B[32;30;40;M", false).unwrap(), @@ -926,6 +952,26 @@ mod tests { ); } + #[cfg(feature = "bracketed-paste")] + #[test] + fn test_parse_csi_bracketed_paste() { + // + assert_eq!( + parse_event(b"\x1B[200~o", false).unwrap(), + None, + "A partial bracketed paste isn't parsed" + ); + assert_eq!( + parse_event(b"\x1B[200~o\x1B[2D", false).unwrap(), + None, + "A partial bracketed paste containing another escape code isn't parsed" + ); + assert_eq!( + parse_event(b"\x1B[200~o\x1B[2D\x1B[201~", false).unwrap(), + Some(InternalEvent::Event(Event::Paste("o\x1B[2D".to_string()))) + ); + } + #[test] fn test_parse_csi_focus() { assert_eq!(