Skip to content

Commit

Permalink
Add bracketed paste parsing (#693)
Browse files Browse the repository at this point in the history
  • Loading branch information
groves authored Aug 10, 2022
1 parent 2a612e0 commit 1fee5ff
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 23 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/crossterm_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ all-features = true
# Features
#
[features]
default = []
default = ["bracketed-paste"]
bracketed-paste = []
event-stream = ["futures-core"]

#
Expand Down Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion examples/event-match-modifiers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 15 additions & 16 deletions examples/event-read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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()) {
Expand All @@ -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<()> {
Expand All @@ -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
Expand All @@ -89,6 +87,7 @@ fn main() -> Result<()> {

execute!(
stdout,
DisableBracketedPaste,
PopKeyboardEnhancementFlags,
DisableFocusChange,
DisableMouseCapture
Expand Down
57 changes: 54 additions & 3 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
//! }
//! }
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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,
Expand All @@ -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),
Expand Down
50 changes: 48 additions & 2 deletions src/event/sys/unix/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,15 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> Result<Option<InternalEvent>> {
} 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),
Expand Down Expand Up @@ -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<Option<InternalEvent>> {
// 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<Option<char>> {
match std::str::from_utf8(buffer) {
Ok(s) => {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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!(
Expand Down

0 comments on commit 1fee5ff

Please sign in to comment.