From 21e5ffdfd8dce7b2cd1a2a74e948e8e071d22161 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 27 Oct 2021 19:20:43 +0200 Subject: [PATCH] fix(input): properly handle bracketed paste (#810) * fix(input): properly handle bracketed paste * style(fmt): make rustfmt happy --- src/tests/e2e/cases.rs | 43 ++++++++ ...j__tests__e2e__cases__bracketed_paste.snap | 29 +++++ zellij-client/src/input_handler.rs | 35 ++---- zellij-client/src/lib.rs | 48 ++------- zellij-client/src/stdin_handler.rs | 102 ++++++++++++++++++ zellij-client/src/unit/input_handler_tests.rs | 64 ----------- 6 files changed, 190 insertions(+), 131 deletions(-) create mode 100644 src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap create mode 100644 zellij-client/src/stdin_handler.rs diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index cdabef81..aa8ad34b 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -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); +} diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap new file mode 100644 index 00000000..9c52cb9a --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: last_snapshot + +--- + Zellij (e2e-test)  Tab #1  +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ ^Tnabc█ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 78869fd5..2b89741e 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -31,7 +31,6 @@ struct InputHandler { command_is_executing: CommandIsExecuting, send_client_instructions: SenderWithContext, 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) { 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; } } } diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index 06f7e32e..45842fec 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -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), SwitchToMode(InputMode), + PastedText(Vec), } 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() diff --git a/zellij-client/src/stdin_handler.rs b/zellij-client/src/stdin_handler.rs new file mode 100644 index 00000000..932550c0 --- /dev/null +++ b/zellij-client/src/stdin_handler.rs @@ -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 { + 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, + send_input_instructions: SenderWithContext, +) { + 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(); + } + } +} diff --git a/zellij-client/src/unit/input_handler_tests.rs b/zellij-client/src/unit/input_handler_tests.rs index f4beb53a..881c7f63 100644 --- a/zellij-client/src/unit/input_handler_tests.rs +++ b/zellij-client/src/unit/input_handler_tests.rs @@ -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" - ); -}