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
|
- 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
|
||||||
|
@ -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"]
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
57
src/event.rs
57
src/event.rs
@ -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),
|
||||||
|
@ -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!(
|
||||||
|
Loading…
Reference in New Issue
Block a user