Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bracketed paste parsing #693

Merged
merged 2 commits into from Aug 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/crossterm_test.yml
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
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
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
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
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
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();
groves marked this conversation as resolved.
Show resolved Hide resolved
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