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 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
41 changes: 24 additions & 17 deletions examples/event-read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use crossterm::event::poll;
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 @@ -34,9 +34,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 @@ -50,18 +50,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 @@ -70,13 +67,23 @@ fn main() -> Result<()> {
enable_raw_mode()?;

let mut stdout = stdout();
execute!(stdout, EnableFocusChange, EnableMouseCapture)?;
execute!(
stdout,
EnableBracketedPaste,
EnableFocusChange,
EnableMouseCapture
)?;

if let Err(e) = print_events() {
println!("Error: {:?}\r", e);
}

execute!(stdout, DisableFocusChange, DisableMouseCapture)?;
execute!(
stdout,
DisableBracketedPaste,
DisableFocusChange,
DisableMouseCapture
)?;

disable_raw_mode()
}
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 @@ -176,11 +176,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 @@ -650,6 +654,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 @@ -773,6 +790,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 @@ -870,6 +896,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