mirror of
https://github.com/zellij-org/zellij.git
synced 2024-12-26 10:43:46 +03:00
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
@ -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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
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"
|
||||
);
|
||||
}
|
||||
|
||||
#[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…
Reference in New Issue
Block a user