Add bracketed paste parsing (#693)
This commit is contained in:
parent
2a612e0f24
commit
1fee5ff30c
3
.github/workflows/crossterm_test.yml
vendored
3
.github/workflows/crossterm_test.yml
vendored
@ -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
|
||||
|
@ -26,7 +26,8 @@ all-features = true
|
||||
# Features
|
||||
#
|
||||
[features]
|
||||
default = []
|
||||
default = ["bracketed-paste"]
|
||||
bracketed-paste = []
|
||||
event-stream = ["futures-core"]
|
||||
|
||||
#
|
||||
@ -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"]
|
||||
|
@ -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 {
|
||||
|
@ -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},
|
||||
@ -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()) {
|
||||
@ -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<()> {
|
||||
@ -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
|
||||
@ -89,6 +87,7 @@ fn main() -> Result<()> {
|
||||
|
||||
execute!(
|
||||
stdout,
|
||||
DisableBracketedPaste,
|
||||
PopKeyboardEnhancementFlags,
|
||||
DisableFocusChange,
|
||||
DisableMouseCapture
|
||||
|
57
src/event.rs
57
src/event.rs
@ -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),
|
||||
//! }
|
||||
//! }
|
||||
@ -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 {
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
@ -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),
|
||||
|
@ -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),
|
||||
@ -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) => {
|
||||
@ -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(),
|
||||
@ -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!(
|
||||
|
Loading…
Reference in New Issue
Block a user