Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(input): properly handle bracketed paste #810

Merged
merged 2 commits into from
Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/tests/e2e/cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 10 additions & 25 deletions zellij-client/src/input_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)>,
}

Expand All @@ -54,7 +53,6 @@ impl InputHandler {
command_is_executing,
send_client_instructions,
should_exit: false,
pasting: false,
receive_input_instructions,
}
}
Expand All @@ -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();
}
Expand All @@ -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
Expand All @@ -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;
}
Expand All @@ -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;
}
}
}
Expand Down
48 changes: 6 additions & 42 deletions zellij-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
};
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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()
Expand Down
102 changes: 102 additions & 0 deletions zellij-client/src/stdin_handler.rs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Loading