Skip to content

Commit 1fee5ff

Browse files
authored
Add bracketed paste parsing (#693)
1 parent 2a612e0 commit 1fee5ff

File tree

6 files changed

+127
-23
lines changed

6 files changed

+127
-23
lines changed

.github/workflows/crossterm_test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ jobs:
6262
- name: Test all features
6363
run: cargo test --all-features -- --nocapture --test-threads 1
6464
continue-on-error: ${{ matrix.can-fail }}
65+
- name: Test no default features
66+
run: cargo test --no-default-features -- --nocapture --test-threads 1
67+
continue-on-error: ${{ matrix.can-fail }}
6568
- name: Test Packaging
6669
if: matrix.rust == 'stable'
6770
run: cargo package

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ all-features = true
2626
# Features
2727
#
2828
[features]
29-
default = []
29+
default = ["bracketed-paste"]
30+
bracketed-paste = []
3031
event-stream = ["futures-core"]
3132

3233
#
@@ -72,6 +73,10 @@ serde_json = "1.0"
7273
#
7374
# Examples
7475
#
76+
[[example]]
77+
name = "event-read"
78+
required-features = ["bracketed-paste"]
79+
7580
[[example]]
7681
name = "event-stream-async-std"
7782
required-features = ["event-stream"]

examples/event-match-modifiers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//!
33
//! cargo run --example event-match-modifiers
44
5-
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
5+
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
66

77
fn match_event(read_event: Event) {
88
match read_event {

examples/event-read.rs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ use crossterm::event::{
1010
use crossterm::{
1111
cursor::position,
1212
event::{
13-
read, DisableFocusChange, DisableMouseCapture, EnableFocusChange, EnableMouseCapture,
14-
Event, KeyCode,
13+
read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
14+
EnableFocusChange, EnableMouseCapture, Event, KeyCode,
1515
},
1616
execute,
1717
terminal::{disable_raw_mode, enable_raw_mode},
@@ -36,9 +36,9 @@ fn print_events() -> Result<()> {
3636
println!("Cursor position: {:?}\r", position());
3737
}
3838

39-
if let Event::Resize(_, _) = event {
40-
let (original_size, new_size) = flush_resize_events(event);
41-
println!("Resize from: {:?}, to: {:?}", original_size, new_size);
39+
if let Event::Resize(x, y) = event {
40+
let (original_size, new_size) = flush_resize_events((x, y));
41+
println!("Resize from: {:?}, to: {:?}\r", original_size, new_size);
4242
}
4343

4444
if event == Event::Key(KeyCode::Esc.into()) {
@@ -52,18 +52,15 @@ fn print_events() -> Result<()> {
5252
// Resize events can occur in batches.
5353
// With a simple loop they can be flushed.
5454
// This function will keep the first and last resize event.
55-
fn flush_resize_events(event: Event) -> ((u16, u16), (u16, u16)) {
56-
if let Event::Resize(x, y) = event {
57-
let mut last_resize = (x, y);
58-
while let Ok(true) = poll(Duration::from_millis(50)) {
59-
if let Ok(Event::Resize(x, y)) = read() {
60-
last_resize = (x, y);
61-
}
55+
fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) {
56+
let mut last_resize = first_resize;
57+
while let Ok(true) = poll(Duration::from_millis(50)) {
58+
if let Ok(Event::Resize(x, y)) = read() {
59+
last_resize = (x, y);
6260
}
63-
64-
return ((x, y), last_resize);
6561
}
66-
((0, 0), (0, 0))
62+
63+
return (first_resize, last_resize);
6764
}
6865

6966
fn main() -> Result<()> {
@@ -74,8 +71,9 @@ fn main() -> Result<()> {
7471
let mut stdout = stdout();
7572
execute!(
7673
stdout,
74+
EnableBracketedPaste,
7775
EnableFocusChange,
78-
EnableMouseCapture,
76+
EnableMouseCapture
7977
PushKeyboardEnhancementFlags(
8078
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
8179
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
@@ -89,6 +87,7 @@ fn main() -> Result<()> {
8987

9088
execute!(
9189
stdout,
90+
DisableBracketedPaste,
9291
PopKeyboardEnhancementFlags,
9392
DisableFocusChange,
9493
DisableMouseCapture

src/event.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
//! Event::FocusLost => println!("FocusLost"),
3939
//! Event::Key(event) => println!("{:?}", event),
4040
//! Event::Mouse(event) => println!("{:?}", event),
41+
//! #[cfg(feature = "bracketed-paste")]
42+
//! Event::Paste(data) => println!("{:?}", data),
4143
//! Event::Resize(width, height) => println!("New size {}x{}", width, height),
4244
//! }
4345
//! }
@@ -63,6 +65,8 @@
6365
//! Event::FocusLost => println!("FocusLost"),
6466
//! Event::Key(event) => println!("{:?}", event),
6567
//! Event::Mouse(event) => println!("{:?}", event),
68+
//! #[cfg(feature = "bracketed-paste")]
69+
//! Event::Paste(data) => println!("Pasted {:?}", data),
6670
//! Event::Resize(width, height) => println!("New size {}x{}", width, height),
6771
//! }
6872
//! } else {
@@ -416,6 +420,8 @@ impl Command for PopKeyboardEnhancementFlags {
416420

417421
/// A command that enables focus event emission.
418422
///
423+
/// It should be paired with [`DisableFocusChange`] at the end of execution.
424+
///
419425
/// Focus events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html).
420426
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421427
pub struct EnableFocusChange;
@@ -433,8 +439,6 @@ impl Command for EnableFocusChange {
433439
}
434440

435441
/// A command that disables focus event emission.
436-
///
437-
/// Focus events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html).
438442
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439443
pub struct DisableFocusChange;
440444

@@ -450,9 +454,52 @@ impl Command for DisableFocusChange {
450454
}
451455
}
452456

457+
/// A command that enables [bracketed paste mode](https://en.wikipedia.org/wiki/Bracketed-paste).
458+
///
459+
/// It should be paired with [`DisableBracketedPaste`] at the end of execution.
460+
///
461+
/// This is not supported in older Windows terminals without
462+
/// [virtual terminal sequences](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences).
463+
#[cfg(feature = "bracketed-paste")]
464+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
465+
pub struct EnableBracketedPaste;
466+
467+
#[cfg(feature = "bracketed-paste")]
468+
impl Command for EnableBracketedPaste {
469+
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
470+
f.write_str(csi!("?2004h"))
471+
}
472+
473+
#[cfg(windows)]
474+
fn execute_winapi(&self) -> Result<()> {
475+
Err(io::Error::new(
476+
io::ErrorKind::Unsupported,
477+
"Bracketed paste not implemented in the legacy Windows API.",
478+
))
479+
}
480+
}
481+
482+
/// A command that disables bracketed paste mode.
483+
#[cfg(feature = "bracketed-paste")]
484+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485+
pub struct DisableBracketedPaste;
486+
487+
#[cfg(feature = "bracketed-paste")]
488+
impl Command for DisableBracketedPaste {
489+
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
490+
f.write_str(csi!("?2004l"))
491+
}
492+
493+
#[cfg(windows)]
494+
fn execute_winapi(&self) -> Result<()> {
495+
Ok(())
496+
}
497+
}
498+
453499
/// Represents an event.
454500
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
455-
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
501+
#[cfg_attr(not(feature = "bracketed-paste"), derive(Copy))]
502+
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)]
456503
pub enum Event {
457504
/// The terminal gained focus
458505
FocusGained,
@@ -462,6 +509,10 @@ pub enum Event {
462509
Key(KeyEvent),
463510
/// A single mouse event with additional pressed modifiers.
464511
Mouse(MouseEvent),
512+
/// A string that was pasted into the terminal. Only emitted if bracketed paste has been
513+
/// enabled.
514+
#[cfg(feature = "bracketed-paste")]
515+
Paste(String),
465516
/// An resize event with new dimensions after resize (columns, rows).
466517
/// **Note** that resize events can be occur in batches.
467518
Resize(u16, u16),

src/event/sys/unix/parse.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,15 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> Result<Option<InternalEvent>> {
177177
} else {
178178
// The final byte of a CSI sequence can be in the range 64-126, so
179179
// let's keep reading anything else.
180-
let last_byte = *buffer.last().unwrap();
180+
let last_byte = buffer[buffer.len() - 1];
181181
if !(64..=126).contains(&last_byte) {
182182
None
183183
} else {
184-
match buffer[buffer.len() - 1] {
184+
#[cfg(feature = "bracketed-paste")]
185+
if buffer.starts_with(b"\x1B[200~") {
186+
return parse_csi_bracketed_paste(buffer);
187+
}
188+
match last_byte {
185189
b'M' => return parse_csi_rxvt_mouse(buffer),
186190
b'~' => return parse_csi_special_key_code(buffer),
187191
b'u' => return parse_csi_u_encoded_key_code(buffer),
@@ -706,6 +710,19 @@ fn parse_cb(cb: u8) -> Result<(MouseEventKind, KeyModifiers)> {
706710
Ok((kind, modifiers))
707711
}
708712

713+
#[cfg(feature = "bracketed-paste")]
714+
pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> Result<Option<InternalEvent>> {
715+
// ESC [ 2 0 0 ~ pasted text ESC 2 0 1 ~
716+
assert!(buffer.starts_with(b"\x1B[200~"));
717+
718+
if !buffer.ends_with(b"\x1b[201~") {
719+
Ok(None)
720+
} else {
721+
let paste = String::from_utf8_lossy(&buffer[6..buffer.len() - 6]).to_string();
722+
Ok(Some(InternalEvent::Event(Event::Paste(paste))))
723+
}
724+
}
725+
709726
pub(crate) fn parse_utf8_char(buffer: &[u8]) -> Result<Option<char>> {
710727
match std::str::from_utf8(buffer) {
711728
Ok(s) => {
@@ -829,6 +846,15 @@ mod tests {
829846
Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))),
830847
);
831848

849+
// parse_csi_bracketed_paste
850+
#[cfg(feature = "bracketed-paste")]
851+
assert_eq!(
852+
parse_event(b"\x1B[200~on and on and on\x1B[201~", false).unwrap(),
853+
Some(InternalEvent::Event(Event::Paste(
854+
"on and on and on".to_string()
855+
))),
856+
);
857+
832858
// parse_csi_rxvt_mouse
833859
assert_eq!(
834860
parse_event(b"\x1B[32;30;40;M", false).unwrap(),
@@ -926,6 +952,26 @@ mod tests {
926952
);
927953
}
928954

955+
#[cfg(feature = "bracketed-paste")]
956+
#[test]
957+
fn test_parse_csi_bracketed_paste() {
958+
//
959+
assert_eq!(
960+
parse_event(b"\x1B[200~o", false).unwrap(),
961+
None,
962+
"A partial bracketed paste isn't parsed"
963+
);
964+
assert_eq!(
965+
parse_event(b"\x1B[200~o\x1B[2D", false).unwrap(),
966+
None,
967+
"A partial bracketed paste containing another escape code isn't parsed"
968+
);
969+
assert_eq!(
970+
parse_event(b"\x1B[200~o\x1B[2D\x1B[201~", false).unwrap(),
971+
Some(InternalEvent::Event(Event::Paste("o\x1B[2D".to_string())))
972+
);
973+
}
974+
929975
#[test]
930976
fn test_parse_csi_focus() {
931977
assert_eq!(

0 commit comments

Comments
 (0)