Add bracketed paste parsing (#693)

This commit is contained in:
Charlie Groves 2022-08-10 07:16:56 +00:00 committed by GitHub
parent 2a612e0f24
commit 1fee5ff30c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 127 additions and 23 deletions

View File

@ -62,6 +62,9 @@ jobs:
- name: Test all features - name: Test all features
run: cargo test --all-features -- --nocapture --test-threads 1 run: cargo test --all-features -- --nocapture --test-threads 1
continue-on-error: ${{ matrix.can-fail }} 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 - name: Test Packaging
if: matrix.rust == 'stable' if: matrix.rust == 'stable'
run: cargo package run: cargo package

View File

@ -26,7 +26,8 @@ all-features = true
# Features # Features
# #
[features] [features]
default = [] default = ["bracketed-paste"]
bracketed-paste = []
event-stream = ["futures-core"] event-stream = ["futures-core"]
# #
@ -72,6 +73,10 @@ serde_json = "1.0"
# #
# Examples # Examples
# #
[[example]]
name = "event-read"
required-features = ["bracketed-paste"]
[[example]] [[example]]
name = "event-stream-async-std" name = "event-stream-async-std"
required-features = ["event-stream"] required-features = ["event-stream"]

View File

@ -2,7 +2,7 @@
//! //!
//! cargo run --example event-match-modifiers //! 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) { fn match_event(read_event: Event) {
match read_event { match read_event {

View File

@ -10,8 +10,8 @@ use crossterm::event::{
use crossterm::{ use crossterm::{
cursor::position, cursor::position,
event::{ event::{
read, DisableFocusChange, DisableMouseCapture, EnableFocusChange, EnableMouseCapture, read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
Event, KeyCode, EnableFocusChange, EnableMouseCapture, Event, KeyCode,
}, },
execute, execute,
terminal::{disable_raw_mode, enable_raw_mode}, terminal::{disable_raw_mode, enable_raw_mode},
@ -36,9 +36,9 @@ fn print_events() -> Result<()> {
println!("Cursor position: {:?}\r", position()); println!("Cursor position: {:?}\r", position());
} }
if let Event::Resize(_, _) = event { if let Event::Resize(x, y) = event {
let (original_size, new_size) = flush_resize_events(event); let (original_size, new_size) = flush_resize_events((x, y));
println!("Resize from: {:?}, to: {:?}", original_size, new_size); println!("Resize from: {:?}, to: {:?}\r", original_size, new_size);
} }
if event == Event::Key(KeyCode::Esc.into()) { if event == Event::Key(KeyCode::Esc.into()) {
@ -52,18 +52,15 @@ fn print_events() -> Result<()> {
// Resize events can occur in batches. // Resize events can occur in batches.
// With a simple loop they can be flushed. // With a simple loop they can be flushed.
// This function will keep the first and last resize event. // This function will keep the first and last resize event.
fn flush_resize_events(event: Event) -> ((u16, u16), (u16, u16)) { fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) {
if let Event::Resize(x, y) = event { let mut last_resize = first_resize;
let mut last_resize = (x, y);
while let Ok(true) = poll(Duration::from_millis(50)) { while let Ok(true) = poll(Duration::from_millis(50)) {
if let Ok(Event::Resize(x, y)) = read() { if let Ok(Event::Resize(x, y)) = read() {
last_resize = (x, y); last_resize = (x, y);
} }
} }
return ((x, y), last_resize); return (first_resize, last_resize);
}
((0, 0), (0, 0))
} }
fn main() -> Result<()> { fn main() -> Result<()> {
@ -74,8 +71,9 @@ fn main() -> Result<()> {
let mut stdout = stdout(); let mut stdout = stdout();
execute!( execute!(
stdout, stdout,
EnableBracketedPaste,
EnableFocusChange, EnableFocusChange,
EnableMouseCapture, EnableMouseCapture
PushKeyboardEnhancementFlags( PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
@ -89,6 +87,7 @@ fn main() -> Result<()> {
execute!( execute!(
stdout, stdout,
DisableBracketedPaste,
PopKeyboardEnhancementFlags, PopKeyboardEnhancementFlags,
DisableFocusChange, DisableFocusChange,
DisableMouseCapture DisableMouseCapture

View File

@ -38,6 +38,8 @@
//! Event::FocusLost => println!("FocusLost"), //! Event::FocusLost => println!("FocusLost"),
//! Event::Key(event) => println!("{:?}", event), //! Event::Key(event) => println!("{:?}", event),
//! Event::Mouse(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), //! Event::Resize(width, height) => println!("New size {}x{}", width, height),
//! } //! }
//! } //! }
@ -63,6 +65,8 @@
//! Event::FocusLost => println!("FocusLost"), //! Event::FocusLost => println!("FocusLost"),
//! Event::Key(event) => println!("{:?}", event), //! Event::Key(event) => println!("{:?}", event),
//! Event::Mouse(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), //! Event::Resize(width, height) => println!("New size {}x{}", width, height),
//! } //! }
//! } else { //! } else {
@ -416,6 +420,8 @@ impl Command for PopKeyboardEnhancementFlags {
/// A command that enables focus event emission. /// 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). /// Focus events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html).
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EnableFocusChange; pub struct EnableFocusChange;
@ -433,8 +439,6 @@ impl Command for EnableFocusChange {
} }
/// A command that disables focus event emission. /// 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DisableFocusChange; 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. /// Represents an event.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[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 { pub enum Event {
/// The terminal gained focus /// The terminal gained focus
FocusGained, FocusGained,
@ -462,6 +509,10 @@ pub enum Event {
Key(KeyEvent), Key(KeyEvent),
/// A single mouse event with additional pressed modifiers. /// A single mouse event with additional pressed modifiers.
Mouse(MouseEvent), 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). /// An resize event with new dimensions after resize (columns, rows).
/// **Note** that resize events can be occur in batches. /// **Note** that resize events can be occur in batches.
Resize(u16, u16), Resize(u16, u16),

View File

@ -177,11 +177,15 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> Result<Option<InternalEvent>> {
} else { } else {
// The final byte of a CSI sequence can be in the range 64-126, so // The final byte of a CSI sequence can be in the range 64-126, so
// let's keep reading anything else. // 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) { if !(64..=126).contains(&last_byte) {
None None
} else { } 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'M' => return parse_csi_rxvt_mouse(buffer),
b'~' => return parse_csi_special_key_code(buffer), b'~' => return parse_csi_special_key_code(buffer),
b'u' => return parse_csi_u_encoded_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)) 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>> { pub(crate) fn parse_utf8_char(buffer: &[u8]) -> Result<Option<char>> {
match std::str::from_utf8(buffer) { match std::str::from_utf8(buffer) {
Ok(s) => { Ok(s) => {
@ -829,6 +846,15 @@ mod tests {
Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))), 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 // parse_csi_rxvt_mouse
assert_eq!( assert_eq!(
parse_event(b"\x1B[32;30;40;M", false).unwrap(), 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] #[test]
fn test_parse_csi_focus() { fn test_parse_csi_focus() {
assert_eq!( assert_eq!(