fix(input): properly handle bracketed paste (#810)

* fix(input): properly handle bracketed paste

* style(fmt): make rustfmt happy
This commit is contained in:
Aram Drevekenin 2021-10-27 19:20:43 +02:00 committed by GitHub
parent 3b1dd1253a
commit 21e5ffdfd8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 190 additions and 131 deletions

View File

@ -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);
}

View File

@ -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.

View File

@ -31,7 +31,6 @@ struct InputHandler {
command_is_executing: CommandIsExecuting,
send_client_instructions: SenderWithContext<ClientInstruction>,
should_exit: bool,
pasting: bool,
receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>,
}
@ -54,7 +53,6 @@ impl InputHandler {
command_is_executing,
send_client_instructions,
should_exit: false,
pasting: false,
receive_input_instructions,
}
}
@ -65,9 +63,6 @@ impl InputHandler {
let mut err_ctx = OPENCALLS.with(|ctx| *ctx.borrow());
err_ctx.add_call(ContextType::StdinHandler);
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 {
self.os_input.enable_mouse();
}
@ -92,12 +87,6 @@ impl InputHandler {
if unsupported_key == alt_left_bracket {
let key = Key::Alt('[');
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 {
// this is a hack because termion doesn't recognize certain keys
// 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)) => {
self.mode = input_mode;
}
@ -121,20 +116,10 @@ impl InputHandler {
}
fn handle_key(&mut self, key: &Key, raw_bytes: Vec<u8>) {
let keybinds = &self.config.keybinds;
if self.pasting {
// we're inside a paste block, if we're in a mode that allows sending text to the
// terminal, send all text directly without interpreting it
// otherwise, just discard the input
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;
}
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;
}
}
}

View File

@ -2,6 +2,7 @@ pub mod os_input_output;
mod command_is_executing;
mod input_handler;
mod stdin_handler;
use log::info;
use std::env::current_exe;
@ -12,15 +13,14 @@ use std::thread;
use crate::{
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_utils::{
channels::{self, ChannelWithContext, SenderWithContext},
consts::{SESSION_NAME, ZELLIJ_IPC_PIPE},
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},
termion,
};
@ -91,9 +91,10 @@ pub enum ClientInfo {
}
#[derive(Debug, Clone)]
pub enum InputInstruction {
pub(crate) enum InputInstruction {
KeyEvent(termion::event::Event, Vec<u8>),
SwitchToMode(InputMode),
PastedText(Vec<u8>),
}
pub fn start_client(
@ -193,44 +194,7 @@ pub fn start_client(
.spawn({
let os_input = os_input.clone();
let send_input_instructions = send_input_instructions.clone();
move || loop {
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();
}
}
move || stdin_loop(os_input, send_input_instructions)
});
let _input_thread = thread::Builder::new()

View 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();
}
}
}

View File

@ -250,67 +250,3 @@ pub fn move_focus_left_in_normal_mode() {
"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"
);
}