fix(input): properly handle bracketed paste (#810)
* fix(input): properly handle bracketed paste * style(fmt): make rustfmt happy
This commit is contained in:
parent
3b1dd1253a
commit
21e5ffdfd8
6 changed files with 190 additions and 131 deletions
|
|
@ -993,3 +993,46 @@ pub fn mirrored_sessions() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
pub fn bracketed_paste() {
|
||||||
|
let fake_win_size = Size {
|
||||||
|
cols: 120,
|
||||||
|
rows: 24,
|
||||||
|
};
|
||||||
|
// here we enter some text, before which we invoke "bracketed paste mode"
|
||||||
|
// we make sure the text in bracketed paste mode is sent directly to the terminal and not
|
||||||
|
// interpreted by us (in this case it will send ^T to the terminal), then we exit bracketed
|
||||||
|
// paste, send some more text and make sure it's also sent to the terminal
|
||||||
|
let last_snapshot = RemoteRunner::new("bracketed_paste", fake_win_size)
|
||||||
|
.add_step(Step {
|
||||||
|
name: "Send pasted text followed by normal text",
|
||||||
|
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
|
||||||
|
let mut step_is_complete = false;
|
||||||
|
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
|
||||||
|
{
|
||||||
|
remote_terminal.send_key(&BRACKETED_PASTE_START);
|
||||||
|
remote_terminal.send_key(&TAB_MODE);
|
||||||
|
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
|
||||||
|
remote_terminal.send_key(&BRACKETED_PASTE_END);
|
||||||
|
remote_terminal.send_key("abc".as_bytes());
|
||||||
|
step_is_complete = true;
|
||||||
|
}
|
||||||
|
step_is_complete
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.add_step(Step {
|
||||||
|
name: "Wait for terminal to render sent keys",
|
||||||
|
instruction: |remote_terminal: RemoteTerminal| -> bool {
|
||||||
|
let mut step_is_complete = false;
|
||||||
|
if remote_terminal.cursor_position_is(9, 2) {
|
||||||
|
// text has been entered into the only terminal pane
|
||||||
|
step_is_complete = true;
|
||||||
|
}
|
||||||
|
step_is_complete
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.run_all_steps();
|
||||||
|
assert_snapshot!(last_snapshot);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
source: src/tests/e2e/cases.rs
|
||||||
|
expression: last_snapshot
|
||||||
|
|
||||||
|
---
|
||||||
|
Zellij (e2e-test) Tab #1
|
||||||
|
┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│$ ^Tnabc█ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SCROLL <o> SESSION <q> QUIT
|
||||||
|
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.
|
||||||
|
|
@ -31,7 +31,6 @@ struct InputHandler {
|
||||||
command_is_executing: CommandIsExecuting,
|
command_is_executing: CommandIsExecuting,
|
||||||
send_client_instructions: SenderWithContext<ClientInstruction>,
|
send_client_instructions: SenderWithContext<ClientInstruction>,
|
||||||
should_exit: bool,
|
should_exit: bool,
|
||||||
pasting: bool,
|
|
||||||
receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
|
receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,7 +53,6 @@ impl InputHandler {
|
||||||
command_is_executing,
|
command_is_executing,
|
||||||
send_client_instructions,
|
send_client_instructions,
|
||||||
should_exit: false,
|
should_exit: false,
|
||||||
pasting: false,
|
|
||||||
receive_input_instructions,
|
receive_input_instructions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,9 +63,6 @@ impl InputHandler {
|
||||||
let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
|
let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
|
||||||
err_ctx.add_call(ContextType::StdinHandler);
|
err_ctx.add_call(ContextType::StdinHandler);
|
||||||
let alt_left_bracket = vec![27, 91];
|
let alt_left_bracket = vec![27, 91];
|
||||||
let bracketed_paste_start = vec![27, 91, 50, 48, 48, 126]; // \u{1b}[200~
|
|
||||||
let bracketed_paste_end = vec![27, 91, 50, 48, 49, 126]; // \u{1b}[201
|
|
||||||
|
|
||||||
if !self.options.disable_mouse_mode {
|
if !self.options.disable_mouse_mode {
|
||||||
self.os_input.enable_mouse();
|
self.os_input.enable_mouse();
|
||||||
}
|
}
|
||||||
|
|
@ -92,12 +87,6 @@ impl InputHandler {
|
||||||
if unsupported_key == alt_left_bracket {
|
if unsupported_key == alt_left_bracket {
|
||||||
let key = Key::Alt('[');
|
let key = Key::Alt('[');
|
||||||
self.handle_key(&key, raw_bytes);
|
self.handle_key(&key, raw_bytes);
|
||||||
} else if unsupported_key == bracketed_paste_start {
|
|
||||||
self.pasting = true;
|
|
||||||
self.handle_unknown_key(raw_bytes);
|
|
||||||
} else if unsupported_key == bracketed_paste_end {
|
|
||||||
self.pasting = false;
|
|
||||||
self.handle_unknown_key(raw_bytes);
|
|
||||||
} else {
|
} else {
|
||||||
// this is a hack because termion doesn't recognize certain keys
|
// this is a hack because termion doesn't recognize certain keys
|
||||||
// in this case we just forward it to the terminal
|
// in this case we just forward it to the terminal
|
||||||
|
|
@ -106,6 +95,12 @@ impl InputHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok((InputInstruction::PastedText(raw_bytes), _error_context)) => {
|
||||||
|
if self.mode == InputMode::Normal || self.mode == InputMode::Locked {
|
||||||
|
let action = Action::Write(raw_bytes);
|
||||||
|
self.dispatch_action(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok((InputInstruction::SwitchToMode(input_mode), _error_context)) => {
|
Ok((InputInstruction::SwitchToMode(input_mode), _error_context)) => {
|
||||||
self.mode = input_mode;
|
self.mode = input_mode;
|
||||||
}
|
}
|
||||||
|
|
@ -121,20 +116,10 @@ impl InputHandler {
|
||||||
}
|
}
|
||||||
fn handle_key(&mut self, key: &Key, raw_bytes: Vec<u8>) {
|
fn handle_key(&mut self, key: &Key, raw_bytes: Vec<u8>) {
|
||||||
let keybinds = &self.config.keybinds;
|
let keybinds = &self.config.keybinds;
|
||||||
if self.pasting {
|
for action in Keybinds::key_to_actions(key, raw_bytes, &self.mode, keybinds) {
|
||||||
// we're inside a paste block, if we're in a mode that allows sending text to the
|
let should_exit = self.dispatch_action(action);
|
||||||
// terminal, send all text directly without interpreting it
|
if should_exit {
|
||||||
// otherwise, just discard the input
|
self.should_exit = true;
|
||||||
if self.mode == InputMode::Normal || self.mode == InputMode::Locked {
|
|
||||||
let action = Action::Write(raw_bytes);
|
|
||||||
self.dispatch_action(action);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for action in Keybinds::key_to_actions(key, raw_bytes, &self.mode, keybinds) {
|
|
||||||
let should_exit = self.dispatch_action(action);
|
|
||||||
if should_exit {
|
|
||||||
self.should_exit = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pub mod os_input_output;
|
||||||
|
|
||||||
mod command_is_executing;
|
mod command_is_executing;
|
||||||
mod input_handler;
|
mod input_handler;
|
||||||
|
mod stdin_handler;
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::env::current_exe;
|
use std::env::current_exe;
|
||||||
|
|
@ -12,15 +13,14 @@ use std::thread;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
command_is_executing::CommandIsExecuting, input_handler::input_loop,
|
command_is_executing::CommandIsExecuting, input_handler::input_loop,
|
||||||
os_input_output::ClientOsApi,
|
os_input_output::ClientOsApi, stdin_handler::stdin_loop,
|
||||||
};
|
};
|
||||||
use termion::input::TermReadEventsAndRaw;
|
|
||||||
use zellij_tile::data::InputMode;
|
use zellij_tile::data::InputMode;
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
channels::{self, ChannelWithContext, SenderWithContext},
|
channels::{self, ChannelWithContext, SenderWithContext},
|
||||||
consts::{SESSION_NAME, ZELLIJ_IPC_PIPE},
|
consts::{SESSION_NAME, ZELLIJ_IPC_PIPE},
|
||||||
errors::{ClientContext, ContextType, ErrorInstruction},
|
errors::{ClientContext, ContextType, ErrorInstruction},
|
||||||
input::{actions::Action, config::Config, mouse::MouseEvent, options::Options},
|
input::{actions::Action, config::Config, options::Options},
|
||||||
ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg},
|
ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg},
|
||||||
termion,
|
termion,
|
||||||
};
|
};
|
||||||
|
|
@ -91,9 +91,10 @@ pub enum ClientInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum InputInstruction {
|
pub(crate) enum InputInstruction {
|
||||||
KeyEvent(termion::event::Event, Vec<u8>),
|
KeyEvent(termion::event::Event, Vec<u8>),
|
||||||
SwitchToMode(InputMode),
|
SwitchToMode(InputMode),
|
||||||
|
PastedText(Vec<u8>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_client(
|
pub fn start_client(
|
||||||
|
|
@ -193,44 +194,7 @@ pub fn start_client(
|
||||||
.spawn({
|
.spawn({
|
||||||
let os_input = os_input.clone();
|
let os_input = os_input.clone();
|
||||||
let send_input_instructions = send_input_instructions.clone();
|
let send_input_instructions = send_input_instructions.clone();
|
||||||
move || loop {
|
move || stdin_loop(os_input, send_input_instructions)
|
||||||
let stdin_buffer = os_input.read_from_stdin();
|
|
||||||
for key_result in stdin_buffer.events_and_raw() {
|
|
||||||
let (key_event, raw_bytes) = key_result.unwrap();
|
|
||||||
if let termion::event::Event::Mouse(me) = key_event {
|
|
||||||
let mouse_event = zellij_utils::input::mouse::MouseEvent::from(me);
|
|
||||||
if let MouseEvent::Hold(_) = mouse_event {
|
|
||||||
// as long as the user is holding the mouse down (no other stdin, eg.
|
|
||||||
// MouseRelease) we need to keep sending this instruction to the app,
|
|
||||||
// because the app itself doesn't have an event loop in the proper
|
|
||||||
// place
|
|
||||||
let mut poller = os_input.stdin_poller();
|
|
||||||
send_input_instructions
|
|
||||||
.send(InputInstruction::KeyEvent(
|
|
||||||
key_event.clone(),
|
|
||||||
raw_bytes.clone(),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
loop {
|
|
||||||
let ready = poller.ready();
|
|
||||||
if ready {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
send_input_instructions
|
|
||||||
.send(InputInstruction::KeyEvent(
|
|
||||||
key_event.clone(),
|
|
||||||
raw_bytes.clone(),
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
send_input_instructions
|
|
||||||
.send(InputInstruction::KeyEvent(key_event, raw_bytes))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let _input_thread = thread::Builder::new()
|
let _input_thread = thread::Builder::new()
|
||||||
|
|
|
||||||
102
zellij-client/src/stdin_handler.rs
Normal file
102
zellij-client/src/stdin_handler.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use crate::os_input_output::ClientOsApi;
|
||||||
|
use crate::InputInstruction;
|
||||||
|
use termion::input::TermReadEventsAndRaw;
|
||||||
|
use zellij_utils::channels::SenderWithContext;
|
||||||
|
use zellij_utils::input::mouse::MouseEvent;
|
||||||
|
use zellij_utils::termion;
|
||||||
|
|
||||||
|
fn bracketed_paste_end_position(stdin_buffer: &[u8]) -> Option<usize> {
|
||||||
|
let bracketed_paste_end = vec![27, 91, 50, 48, 49, 126]; // \u{1b}[201
|
||||||
|
let mut bp_position = 0;
|
||||||
|
let mut position = None;
|
||||||
|
for (i, byte) in stdin_buffer.iter().enumerate() {
|
||||||
|
if Some(byte) == bracketed_paste_end.get(bp_position) {
|
||||||
|
position = Some(i);
|
||||||
|
bp_position += 1;
|
||||||
|
if bp_position == bracketed_paste_end.len() - 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bp_position = 0;
|
||||||
|
position = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bp_position == bracketed_paste_end.len() - 1 {
|
||||||
|
position
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stdin_loop(
|
||||||
|
os_input: Box<dyn ClientOsApi>,
|
||||||
|
send_input_instructions: SenderWithContext<InputInstruction>,
|
||||||
|
) {
|
||||||
|
let mut pasting = false;
|
||||||
|
let bracketed_paste_start = vec![27, 91, 50, 48, 48, 126]; // \u{1b}[200~
|
||||||
|
loop {
|
||||||
|
let mut stdin_buffer = os_input.read_from_stdin();
|
||||||
|
if pasting
|
||||||
|
|| (stdin_buffer.len() > bracketed_paste_start.len()
|
||||||
|
&& stdin_buffer
|
||||||
|
.iter()
|
||||||
|
.take(bracketed_paste_start.len())
|
||||||
|
.eq(bracketed_paste_start.iter()))
|
||||||
|
{
|
||||||
|
match bracketed_paste_end_position(&stdin_buffer) {
|
||||||
|
Some(paste_end_position) => {
|
||||||
|
let pasted_input = stdin_buffer.drain(..=paste_end_position).collect();
|
||||||
|
send_input_instructions
|
||||||
|
.send(InputInstruction::PastedText(pasted_input))
|
||||||
|
.unwrap();
|
||||||
|
pasting = false;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
send_input_instructions
|
||||||
|
.send(InputInstruction::PastedText(stdin_buffer))
|
||||||
|
.unwrap();
|
||||||
|
pasting = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stdin_buffer.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for key_result in stdin_buffer.events_and_raw() {
|
||||||
|
let (key_event, raw_bytes) = key_result.unwrap();
|
||||||
|
if let termion::event::Event::Mouse(me) = key_event {
|
||||||
|
let mouse_event = zellij_utils::input::mouse::MouseEvent::from(me);
|
||||||
|
if let MouseEvent::Hold(_) = mouse_event {
|
||||||
|
// as long as the user is holding the mouse down (no other stdin, eg.
|
||||||
|
// MouseRelease) we need to keep sending this instruction to the app,
|
||||||
|
// because the app itself doesn't have an event loop in the proper
|
||||||
|
// place
|
||||||
|
let mut poller = os_input.stdin_poller();
|
||||||
|
send_input_instructions
|
||||||
|
.send(InputInstruction::KeyEvent(
|
||||||
|
key_event.clone(),
|
||||||
|
raw_bytes.clone(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
loop {
|
||||||
|
let ready = poller.ready();
|
||||||
|
if ready {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
send_input_instructions
|
||||||
|
.send(InputInstruction::KeyEvent(
|
||||||
|
key_event.clone(),
|
||||||
|
raw_bytes.clone(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send_input_instructions
|
||||||
|
.send(InputInstruction::KeyEvent(key_event, raw_bytes))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -250,67 +250,3 @@ pub fn move_focus_left_in_normal_mode() {
|
||||||
"All actions sent to server properly"
|
"All actions sent to server properly"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn bracketed_paste() {
|
|
||||||
let stdin_events = vec![
|
|
||||||
(
|
|
||||||
commands::BRACKETED_PASTE_START.to_vec(),
|
|
||||||
Event::Unsupported(commands::BRACKETED_PASTE_START.to_vec()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(),
|
|
||||||
Event::Key(Key::Alt('h')),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
commands::BRACKETED_PASTE_END.to_vec(),
|
|
||||||
Event::Unsupported(commands::BRACKETED_PASTE_END.to_vec()),
|
|
||||||
),
|
|
||||||
(commands::QUIT.to_vec(), Event::Key(Key::Ctrl('q'))),
|
|
||||||
];
|
|
||||||
let events_sent_to_server = Arc::new(Mutex::new(vec![]));
|
|
||||||
let command_is_executing = CommandIsExecuting::new();
|
|
||||||
let client_os_api = Box::new(FakeClientOsApi::new(
|
|
||||||
events_sent_to_server.clone(),
|
|
||||||
command_is_executing.clone(),
|
|
||||||
));
|
|
||||||
let config = Config::from_default_assets().unwrap();
|
|
||||||
let options = Options::default();
|
|
||||||
|
|
||||||
let (send_client_instructions, _receive_client_instructions): ChannelWithContext<
|
|
||||||
ClientInstruction,
|
|
||||||
> = channels::bounded(50);
|
|
||||||
let send_client_instructions = SenderWithContext::new(send_client_instructions);
|
|
||||||
|
|
||||||
let (send_input_instructions, receive_input_instructions): ChannelWithContext<
|
|
||||||
InputInstruction,
|
|
||||||
> = channels::bounded(50);
|
|
||||||
let send_input_instructions = SenderWithContext::new(send_input_instructions);
|
|
||||||
for event in stdin_events {
|
|
||||||
send_input_instructions
|
|
||||||
.send(InputInstruction::KeyEvent(event.1, event.0))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let default_mode = InputMode::Normal;
|
|
||||||
input_loop(
|
|
||||||
client_os_api,
|
|
||||||
config,
|
|
||||||
options,
|
|
||||||
command_is_executing,
|
|
||||||
send_client_instructions,
|
|
||||||
default_mode,
|
|
||||||
receive_input_instructions,
|
|
||||||
);
|
|
||||||
let expected_actions_sent_to_server = vec![
|
|
||||||
Action::Write(commands::BRACKETED_PASTE_START.to_vec()),
|
|
||||||
Action::Write(commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec()), // keys were directly written to server and not interpreted
|
|
||||||
Action::Write(commands::BRACKETED_PASTE_END.to_vec()),
|
|
||||||
Action::Quit,
|
|
||||||
];
|
|
||||||
let received_actions = extract_actions_sent_to_server(events_sent_to_server);
|
|
||||||
assert_eq!(
|
|
||||||
expected_actions_sent_to_server, received_actions,
|
|
||||||
"All actions sent to server properly"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue