feat(plugins): rebind keys at runtime (#3422)

* refactor(server): interpret keys on server so they can be rebound

* feat(plugins): allow rebinding keys at runtime

* various cleanups

* add tests

* style(fmt): rustfmt

* fix(tests): address (some) e2e test flakiness

* style(fmt): rustfmt
This commit is contained in:
Aram Drevekenin 2024-06-14 17:11:02 +02:00 committed by GitHub
parent 2ac8b15191
commit 1f0ae94f01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 463 additions and 427 deletions

View File

@ -56,6 +56,7 @@ impl ZellijPlugin for State {
PermissionType::WebAccess,
PermissionType::ReadCliPipes,
PermissionType::MessageAndLaunchOtherPlugins,
PermissionType::RebindKeys,
]);
self.configuration = configuration;
subscribe(&[
@ -318,6 +319,18 @@ impl ZellijPlugin for State {
Some(std::path::PathBuf::from("/tmp")),
);
},
BareKey::Char('0') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
rebind_keys(
"
keybinds {
locked {
bind \"a\" { NewTab; }
}
}
"
.to_owned(),
);
},
_ => {},
},
Event::CustomMessage(message, payload) => {

View File

@ -162,6 +162,7 @@ pub fn split_terminals_vertically() {
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
// back to normal mode after split
step_is_complete = true;
@ -205,6 +206,7 @@ pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() {
let mut step_is_complete = false;
if remote_terminal.cursor_position_is(3, 2) {
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
// back to normal mode after split
step_is_complete = true;
@ -259,6 +261,7 @@ pub fn scrolling_inside_a_pane() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -286,6 +289,7 @@ pub fn scrolling_inside_a_pane() {
{
// all lines have been written to the pane
remote_terminal.send_key(&SCROLL_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SCROLL_UP_IN_SCROLL_MODE);
step_is_complete = true;
}
@ -337,6 +341,7 @@ pub fn toggle_pane_fullscreen() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -350,6 +355,7 @@ pub fn toggle_pane_fullscreen() {
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE);
step_is_complete = true;
}
@ -398,6 +404,7 @@ pub fn open_new_tab() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -411,6 +418,7 @@ pub fn open_new_tab() {
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane
remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
step_is_complete = true;
}
@ -463,6 +471,7 @@ pub fn close_tab() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -476,6 +485,7 @@ pub fn close_tab() {
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane
remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
step_is_complete = true;
}
@ -493,6 +503,7 @@ pub fn close_tab() {
{
// cursor is in the newly opened second tab
remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&CLOSE_TAB_IN_TAB_MODE);
step_is_complete = true;
}
@ -653,6 +664,7 @@ pub fn close_pane() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -666,6 +678,7 @@ pub fn close_pane() {
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&CLOSE_PANE_IN_PANE_MODE);
step_is_complete = true;
}
@ -751,6 +764,7 @@ pub fn closing_last_pane_exits_zellij() {
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&CLOSE_PANE_IN_PANE_MODE);
step_is_complete = true;
}
@ -795,6 +809,7 @@ pub fn typing_exit_closes_pane() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -807,9 +822,13 @@ pub fn typing_exit_closes_pane() {
let mut step_is_complete = false;
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
remote_terminal.send_key("e".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("x".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("i".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("t".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("\n".as_bytes());
step_is_complete = true;
}
@ -859,6 +878,7 @@ pub fn resize_pane() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -872,6 +892,7 @@ pub fn resize_pane() {
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane
remote_terminal.send_key(&RESIZE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&RESIZE_LEFT_IN_RESIZE_MODE);
// back to normal mode
remote_terminal.send_key(&ENTER);
@ -933,7 +954,9 @@ pub fn lock_mode() {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("INTERFACE LOCKED") {
remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("abc".as_bytes());
step_is_complete = true;
}
@ -983,6 +1006,7 @@ pub fn resize_terminal_window() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -1046,6 +1070,7 @@ pub fn detach_and_attach_session() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -1070,6 +1095,7 @@ pub fn detach_and_attach_session() {
let mut step_is_complete = false;
if remote_terminal.cursor_position_is(77, 2) {
remote_terminal.send_key(&SESSION_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&DETACH_IN_SESSION_MODE);
// text has been entered
step_is_complete = true;
@ -1286,6 +1312,7 @@ fn focus_pane_with_mouse() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -1345,6 +1372,7 @@ pub fn scrolling_inside_a_pane_with_mouse() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -1420,6 +1448,7 @@ pub fn start_without_pane_frames() {
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 1)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -1489,6 +1518,7 @@ pub fn mirrored_sessions() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -1502,6 +1532,7 @@ pub fn mirrored_sessions() {
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane
remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
step_is_complete = true;
}
@ -1544,6 +1575,7 @@ pub fn mirrored_sessions() {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("some text") {
remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&MOVE_FOCUS_LEFT_IN_PANE_MODE); // same key as tab mode
step_is_complete = true;
}
@ -1726,6 +1758,7 @@ pub fn multiple_users_in_different_panes_and_same_tab() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true;
}
@ -1817,6 +1850,7 @@ pub fn multiple_users_in_different_tabs() {
if remote_terminal.cursor_position_is(3, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane
remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
step_is_complete = true;
}
@ -1839,6 +1873,7 @@ pub fn multiple_users_in_different_tabs() {
&& remote_terminal.snapshot_contains("Tab #1 [ ]")
&& remote_terminal.snapshot_contains("Tab #2")
&& remote_terminal.status_bar_appears()
&& !remote_terminal.snapshot_contains("AND:")
{
// cursor is in the newly opened second tab
step_is_complete = true;
@ -1899,11 +1934,17 @@ pub fn bracketed_paste() {
&& remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&BRACKETED_PASTE_START);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("a".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("b".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("c".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&BRACKETED_PASTE_END);
step_is_complete = true;
}
@ -1952,6 +1993,7 @@ pub fn toggle_floating_panes() {
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&TOGGLE_FLOATING_PANES);
// back to normal mode after split
step_is_complete = true;
@ -2002,6 +2044,7 @@ pub fn tmux_mode() {
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&TMUX_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_TMUX_MODE);
// back to normal mode after split
step_is_complete = true;
@ -2050,6 +2093,7 @@ pub fn edit_scrollback() {
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&SCROLL_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&EDIT_SCROLLBACK);
step_is_complete = true;
}
@ -2099,8 +2143,13 @@ pub fn undo_rename_tab() {
&& remote_terminal.snapshot_contains("Tab #1")
{
remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&RENAME_TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&[97, 97]);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&ESC);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&ESC);
step_is_complete = true;
}
@ -2113,7 +2162,9 @@ pub fn undo_rename_tab() {
name: "Wait for tab name to apper on screen",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("Tab #1") {
if remote_terminal.snapshot_contains("Tab #1")
&& remote_terminal.snapshot_contains("Tip:")
{
step_is_complete = true
}
step_is_complete
@ -2149,8 +2200,13 @@ pub fn undo_rename_pane() {
if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{
remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&RENAME_PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&[97, 97]);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&ESC);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&ESC);
step_is_complete = true;
}
@ -2163,7 +2219,9 @@ pub fn undo_rename_pane() {
name: "Wait for pane name to apper on screen",
instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("Pane #1") {
if remote_terminal.snapshot_contains("Pane #1")
&& remote_terminal.snapshot_contains("Tip:")
{
step_is_complete = true
}
step_is_complete
@ -2212,6 +2270,7 @@ pub fn send_command_through_the_cli() {
"{}/append-echo-script.sh",
fixture_folder
));
std::thread::sleep(std::time::Duration::from_millis(100));
step_is_complete = true;
}
step_is_complete
@ -2221,11 +2280,7 @@ pub fn send_command_through_the_cli() {
name: "Initial run of suspended command",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("<Ctrl-c>")
&& remote_terminal.cursor_position_is(0, 0)
// cursor does not appear in
// suspend_start panes
{
if remote_terminal.snapshot_contains("<Ctrl-c>") {
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPACE); // run script - here we use SPACE
// instead of the default ENTER because

View File

@ -261,7 +261,7 @@ fn read_from_channel(
break;
}
if should_sleep {
std::thread::sleep(std::time::Duration::from_millis(10));
std::thread::sleep(std::time::Duration::from_millis(100));
should_sleep = false;
}
let mut buf = [0u8; 1280000];

View File

@ -11,6 +11,7 @@ pub fn new_tab() -> Step {
let mut step_is_complete = false;
if remote_terminal.tip_appears() && remote_terminal.status_bar_appears() {
remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
step_is_complete = true;
}
@ -37,6 +38,7 @@ pub fn move_tab_left() -> Step {
let mut step_is_complete = false;
if remote_terminal.tip_appears() && remote_terminal.status_bar_appears() {
remote_terminal.send_key(&MOVE_TAB_LEFT);
std::thread::sleep(std::time::Duration::from_millis(100));
step_is_complete = true;
}
step_is_complete

View File

@ -146,9 +146,6 @@ impl InputHandler {
)) => {
self.handle_key(&key_with_modifier, raw_bytes, true);
},
Ok((InputInstruction::SwitchToMode(input_mode), _error_context)) => {
self.mode = input_mode;
},
Ok((
InputInstruction::AnsiStdinInstructions(ansi_stdin_instructions),
_error_context,
@ -180,18 +177,13 @@ impl InputHandler {
raw_bytes: Vec<u8>,
is_kitty_keyboard_protocol: bool,
) {
let keybinds = &self.config.keybinds;
for action in keybinds.get_actions_for_key_in_mode_or_default_action(
&self.mode,
key,
// we interpret the keys into actions on the server side so that we can change the
// keybinds at runtime
self.os_input.send_to_server(ClientToServerMsg::Key(
key.clone(),
raw_bytes,
is_kitty_keyboard_protocol,
) {
let should_exit = self.dispatch_action(action, None);
if should_exit {
self.should_exit = true;
}
}
));
}
fn handle_stdin_ansi_instruction(&mut self, ansi_stdin_instructions: AnsiStdinInstruction) {
match ansi_stdin_instructions {
@ -316,14 +308,8 @@ impl InputHandler {
self.exit(ExitReason::NormalDetached);
should_break = true;
},
Action::SwitchToMode(mode) => {
// this is an optimistic update, we should get a SwitchMode instruction from the
// server later that atomically changes the mode as well
self.mode = mode;
self.os_input
.send_to_server(ClientToServerMsg::Action(action, None, None));
},
Action::CloseFocus
| Action::SwitchToMode(..)
| Action::ClearScreen
| Action::NewPane(..)
| Action::Run(_)

View File

@ -25,7 +25,7 @@ use crate::{
use zellij_utils::{
channels::{self, ChannelWithContext, SenderWithContext},
consts::{set_permissions, ZELLIJ_SOCK_DIR},
data::{ClientId, ConnectToSession, InputMode, KeyWithModifier, Style},
data::{ClientId, ConnectToSession, KeyWithModifier, Style},
envs,
errors::{ClientContext, ContextType, ErrorInstruction},
input::{config::Config, options::Options},
@ -42,7 +42,6 @@ pub(crate) enum ClientInstruction {
Render(String),
UnblockInputThread,
Exit(ExitReason),
SwitchToMode(InputMode),
Connected,
ActiveClients(Vec<ClientId>),
StartedParsingStdinQuery,
@ -62,9 +61,6 @@ impl From<ServerToClientMsg> for ClientInstruction {
ServerToClientMsg::Exit(e) => ClientInstruction::Exit(e),
ServerToClientMsg::Render(buffer) => ClientInstruction::Render(buffer),
ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread,
ServerToClientMsg::SwitchToMode(input_mode) => {
ClientInstruction::SwitchToMode(input_mode)
},
ServerToClientMsg::Connected => ClientInstruction::Connected,
ServerToClientMsg::ActiveClients(clients) => ClientInstruction::ActiveClients(clients),
ServerToClientMsg::Log(log_lines) => ClientInstruction::Log(log_lines),
@ -90,7 +86,6 @@ impl From<&ClientInstruction> for ClientContext {
ClientInstruction::Error(_) => ClientContext::Error,
ClientInstruction::Render(_) => ClientContext::Render,
ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread,
ClientInstruction::SwitchToMode(_) => ClientContext::SwitchToMode,
ClientInstruction::Connected => ClientContext::Connected,
ClientInstruction::ActiveClients(_) => ClientContext::ActiveClients,
ClientInstruction::Log(_) => ClientContext::Log,
@ -154,7 +149,6 @@ impl ClientInfo {
pub(crate) enum InputInstruction {
KeyEvent(InputEvent, Vec<u8>),
KeyWithModifierEvent(KeyWithModifier, Vec<u8>),
SwitchToMode(InputMode),
AnsiStdinInstructions(Vec<AnsiStdinInstruction>),
StartedParsing,
DoneParsing,
@ -505,11 +499,6 @@ pub fn start_client(
ClientInstruction::UnblockInputThread => {
command_is_executing.unblock_input_thread();
},
ClientInstruction::SwitchToMode(input_mode) => {
send_input_instructions
.send(InputInstruction::SwitchToMode(input_mode))
.unwrap();
},
ClientInstruction::Log(lines_to_log) => {
for line in lines_to_log {
log::info!("{line}");
@ -634,7 +623,3 @@ pub fn start_server_detached(
os_input.connect_to_server(&*ipc_pipe);
os_input.send_to_server(first_msg);
}
#[cfg(test)]
#[path = "./unit/stdin_tests.rs"]
mod stdin_tests;

View File

@ -1,320 +0,0 @@
use super::input_loop;
use crate::stdin_ansi_parser::StdinAnsiParser;
use crate::stdin_loop;
use zellij_utils::anyhow::Result;
use zellij_utils::data::{Direction, InputMode, Palette};
use zellij_utils::input::actions::Action;
use zellij_utils::input::config::Config;
use zellij_utils::input::options::Options;
use zellij_utils::nix;
use zellij_utils::pane_size::Size;
use zellij_utils::termwiz::input::{InputEvent, KeyCode, KeyEvent, Modifiers};
use crate::InputInstruction;
use crate::{
os_input_output::{ClientOsApi, StdinPoller},
ClientInstruction, CommandIsExecuting,
};
use ::insta::assert_snapshot;
use std::path::Path;
use std::io;
use std::os::unix::io::RawFd;
use std::sync::{Arc, Mutex};
use std::thread;
use zellij_utils::{
errors::ErrorContext,
ipc::{ClientToServerMsg, ServerToClientMsg},
};
use zellij_utils::channels::{self, ChannelWithContext, SenderWithContext};
fn read_fixture(fixture_name: &str) -> Vec<u8> {
let mut path_to_file = std::path::PathBuf::new();
path_to_file.push("../src");
path_to_file.push("tests");
path_to_file.push("fixtures");
path_to_file.push(fixture_name);
std::fs::read(path_to_file)
.unwrap_or_else(|_| panic!("could not read fixture {:?}", &fixture_name))
}
#[allow(unused)]
pub mod commands {
pub const QUIT: [u8; 1] = [17]; // ctrl-q
pub const ESC: [u8; 1] = [27];
pub const ENTER: [u8; 1] = [10]; // char '\n'
pub const MOVE_FOCUS_LEFT_IN_NORMAL_MODE: [u8; 2] = [27, 104]; // alt-h
pub const MOVE_FOCUS_RIGHT_IN_NORMAL_MODE: [u8; 2] = [27, 108]; // alt-l
pub const PANE_MODE: [u8; 1] = [16]; // ctrl-p
pub const SPAWN_TERMINAL_IN_PANE_MODE: [u8; 1] = [110]; // n
pub const MOVE_FOCUS_IN_PANE_MODE: [u8; 1] = [112]; // p
pub const SPLIT_DOWN_IN_PANE_MODE: [u8; 1] = [100]; // d
pub const SPLIT_RIGHT_IN_PANE_MODE: [u8; 1] = [114]; // r
pub const TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE: [u8; 1] = [102]; // f
pub const CLOSE_PANE_IN_PANE_MODE: [u8; 1] = [120]; // x
pub const MOVE_FOCUS_DOWN_IN_PANE_MODE: [u8; 1] = [106]; // j
pub const MOVE_FOCUS_UP_IN_PANE_MODE: [u8; 1] = [107]; // k
pub const MOVE_FOCUS_LEFT_IN_PANE_MODE: [u8; 1] = [104]; // h
pub const MOVE_FOCUS_RIGHT_IN_PANE_MODE: [u8; 1] = [108]; // l
pub const SCROLL_MODE: [u8; 1] = [19]; // ctrl-s
pub const SCROLL_UP_IN_SCROLL_MODE: [u8; 1] = [107]; // k
pub const SCROLL_DOWN_IN_SCROLL_MODE: [u8; 1] = [106]; // j
pub const SCROLL_PAGE_UP_IN_SCROLL_MODE: [u8; 1] = [2]; // ctrl-b
pub const SCROLL_PAGE_DOWN_IN_SCROLL_MODE: [u8; 1] = [6]; // ctrl-f
pub const RESIZE_MODE: [u8; 1] = [18]; // ctrl-r
pub const RESIZE_DOWN_IN_RESIZE_MODE: [u8; 1] = [106]; // j
pub const RESIZE_UP_IN_RESIZE_MODE: [u8; 1] = [107]; // k
pub const RESIZE_LEFT_IN_RESIZE_MODE: [u8; 1] = [104]; // h
pub const RESIZE_RIGHT_IN_RESIZE_MODE: [u8; 1] = [108]; // l
pub const TAB_MODE: [u8; 1] = [20]; // ctrl-t
pub const NEW_TAB_IN_TAB_MODE: [u8; 1] = [110]; // n
pub const SWITCH_NEXT_TAB_IN_TAB_MODE: [u8; 1] = [108]; // l
pub const SWITCH_PREV_TAB_IN_TAB_MODE: [u8; 1] = [104]; // h
pub const CLOSE_TAB_IN_TAB_MODE: [u8; 1] = [120]; // x
pub const BRACKETED_PASTE_START: [u8; 6] = [27, 91, 50, 48, 48, 126]; // \u{1b}[200~
pub const BRACKETED_PASTE_END: [u8; 6] = [27, 91, 50, 48, 49, 126]; // \u{1b}[201
pub const SLEEP: [u8; 0] = [];
}
#[derive(Default, Clone)]
struct FakeStdoutWriter {
buffer: Arc<Mutex<Vec<u8>>>,
}
impl FakeStdoutWriter {
pub fn new(buffer: Arc<Mutex<Vec<u8>>>) -> Self {
FakeStdoutWriter { buffer }
}
}
impl io::Write for FakeStdoutWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
self.buffer.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
}
#[derive(Clone)]
struct FakeClientOsApi {
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
command_is_executing: Arc<Mutex<CommandIsExecuting>>,
stdout_buffer: Arc<Mutex<Vec<u8>>>,
stdin_buffer: Vec<u8>,
}
impl FakeClientOsApi {
pub fn new(
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
command_is_executing: CommandIsExecuting,
) -> Self {
// while command_is_executing itself is implemented with an Arc<Mutex>, we have to have an
// Arc<Mutex> here because we need interior mutability, otherwise we'll have to change the
// ClientOsApi trait, and that will cause a lot of havoc
let command_is_executing = Arc::new(Mutex::new(command_is_executing));
let stdout_buffer = Arc::new(Mutex::new(vec![]));
FakeClientOsApi {
events_sent_to_server,
command_is_executing,
stdout_buffer,
stdin_buffer: vec![],
}
}
pub fn with_stdin_buffer(mut self, stdin_buffer: Vec<u8>) -> Self {
self.stdin_buffer = stdin_buffer;
self
}
pub fn stdout_buffer(&self) -> Vec<u8> {
self.stdout_buffer.lock().unwrap().drain(..).collect()
}
}
impl ClientOsApi for FakeClientOsApi {
fn get_terminal_size_using_fd(&self, _fd: RawFd) -> Size {
unimplemented!()
}
fn set_raw_mode(&mut self, _fd: RawFd) {
unimplemented!()
}
fn unset_raw_mode(&self, _fd: RawFd) -> Result<(), nix::Error> {
unimplemented!()
}
fn get_stdout_writer(&self) -> Box<dyn io::Write> {
let fake_stdout_writer = FakeStdoutWriter::new(self.stdout_buffer.clone());
Box::new(fake_stdout_writer)
}
fn get_stdin_reader(&self) -> Box<dyn io::BufRead> {
unimplemented!()
}
fn update_session_name(&mut self, _new_session_name: String) {}
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> {
Ok(self.stdin_buffer.drain(..).collect())
}
fn box_clone(&self) -> Box<dyn ClientOsApi> {
unimplemented!()
}
fn send_to_server(&self, msg: ClientToServerMsg) {
{
let mut events_sent_to_server = self.events_sent_to_server.lock().unwrap();
events_sent_to_server.push(msg);
}
{
let mut command_is_executing = self.command_is_executing.lock().unwrap();
command_is_executing.unblock_input_thread();
}
}
fn recv_from_server(&self) -> Option<(ServerToClientMsg, ErrorContext)> {
unimplemented!()
}
fn handle_signals(&self, _sigwinch_cb: Box<dyn Fn()>, _quit_cb: Box<dyn Fn()>) {
unimplemented!()
}
fn connect_to_server(&self, _path: &Path) {
unimplemented!()
}
fn load_palette(&self) -> Palette {
unimplemented!()
}
fn enable_mouse(&self) -> Result<()> {
Ok(())
}
fn disable_mouse(&self) -> Result<()> {
Ok(())
}
fn stdin_poller(&self) -> StdinPoller {
unimplemented!()
}
}
fn extract_actions_sent_to_server(
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
) -> Vec<Action> {
let events_sent_to_server = events_sent_to_server.lock().unwrap();
events_sent_to_server.iter().fold(vec![], |mut acc, event| {
if let ClientToServerMsg::Action(action, None, None) = event {
acc.push(action.clone());
}
acc
})
}
#[test]
pub fn quit_breaks_input_loop() {
let stdin_events = vec![(
commands::QUIT.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('q'),
modifiers: Modifiers::CTRL,
}),
)];
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::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"
);
}
#[test]
pub fn move_focus_left_in_normal_mode() {
let stdin_events = vec![
(
commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('h'),
modifiers: Modifiers::ALT,
}),
),
(
commands::QUIT.to_vec(),
InputEvent::Key(KeyEvent {
key: KeyCode::Char('q'),
modifiers: Modifiers::CTRL,
}),
),
];
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::MoveFocusOrTab(Direction::Left), 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"
);
}

View File

@ -42,12 +42,13 @@ use zellij_utils::{
channels::{self, ChannelWithContext, SenderWithContext},
cli::CliArgs,
consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE},
data::{ConnectToSession, Event, PluginCapabilities},
data::{ConnectToSession, Event, InputMode, PluginCapabilities},
errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext},
home::{default_layout_dir, get_default_data_dir},
input::{
command::{RunCommand, TerminalAction},
get_mode_info,
keybinds::Keybinds,
layout::Layout,
options::Options,
plugins::PluginAliases,
@ -94,6 +95,9 @@ pub enum ServerInstruction {
client_id: ClientId,
},
DisconnectAllClientsExcept(ClientId),
ChangeMode(ClientId, InputMode),
ChangeModeForAllClients(InputMode),
RebindKeys(ClientId, String), // String -> stringified keybindings
}
impl From<&ServerInstruction> for ServerContext {
@ -121,6 +125,11 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::DisconnectAllClientsExcept(..) => {
ServerContext::DisconnectAllClientsExcept
},
ServerInstruction::ChangeMode(..) => ServerContext::ChangeMode,
ServerInstruction::ChangeModeForAllClients(..) => {
ServerContext::ChangeModeForAllClients
},
ServerInstruction::RebindKeys(..) => ServerContext::RebindKeys,
}
}
}
@ -138,6 +147,8 @@ pub(crate) struct SessionMetaData {
pub default_shell: Option<TerminalAction>,
pub layout: Box<Layout>,
pub config_options: Box<Options>,
pub client_keybinds: HashMap<ClientId, Keybinds>,
pub client_input_modes: HashMap<ClientId, InputMode>,
screen_thread: Option<thread::JoinHandle<()>>,
pty_thread: Option<thread::JoinHandle<()>>,
plugin_thread: Option<thread::JoinHandle<()>>,
@ -145,6 +156,56 @@ pub(crate) struct SessionMetaData {
background_jobs_thread: Option<thread::JoinHandle<()>>,
}
impl SessionMetaData {
pub fn set_client_keybinds(&mut self, client_id: ClientId, keybinds: Keybinds) {
self.client_keybinds.insert(client_id, keybinds);
self.client_input_modes.insert(
client_id,
self.config_options.default_mode.unwrap_or_default(),
);
}
pub fn get_client_keybinds_and_mode(
&self,
client_id: &ClientId,
) -> Option<(&Keybinds, &InputMode)> {
match (
self.client_keybinds.get(client_id),
self.client_input_modes.get(client_id),
) {
(Some(client_keybinds), Some(client_input_mode)) => {
Some((client_keybinds, client_input_mode))
},
_ => None,
}
}
pub fn change_mode_for_all_clients(&mut self, input_mode: InputMode) {
let all_clients: Vec<ClientId> = self.client_input_modes.keys().copied().collect();
for client_id in all_clients {
self.client_input_modes.insert(client_id, input_mode);
}
}
pub fn rebind_keys(&mut self, client_id: ClientId, new_keybinds: String) -> Option<Keybinds> {
if let Some(current_keybinds) = self.client_keybinds.get_mut(&client_id) {
match Keybinds::from_string(
new_keybinds,
current_keybinds.clone(),
&self.config_options,
) {
Ok(new_keybinds) => {
*current_keybinds = new_keybinds.clone();
return Some(new_keybinds);
},
Err(e) => {
log::error!("Failed to parse keybindings: {}", e);
},
}
} else {
log::error!("Failed to bind keys for client: {client_id}");
}
None
}
}
impl Drop for SessionMetaData {
fn drop(&mut self) {
let _ = self.senders.send_to_pty(PtyInstruction::Exit);
@ -383,6 +444,12 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
plugin_aliases,
);
*session_data.write().unwrap() = Some(session);
session_data
.write()
.unwrap()
.as_mut()
.unwrap()
.set_client_keybinds(client_id, client_attributes.keybinds.clone());
session_state
.write()
.unwrap()
@ -469,8 +536,9 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
pane_id_to_focus,
client_id,
) => {
let rlock = session_data.read().unwrap();
let session_data = rlock.as_ref().unwrap();
let mut rlock = session_data.write().unwrap();
let session_data = rlock.as_mut().unwrap();
session_data.set_client_keybinds(client_id, attrs.keybinds.clone());
session_state
.write()
.unwrap()
@ -498,7 +566,6 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.unwrap();
let default_mode = options.default_mode.unwrap_or_default();
let mode_info = get_mode_info(default_mode, &attrs, session_data.capabilities);
let mode = mode_info.mode;
session_data
.senders
.send_to_screen(ScreenInstruction::ChangeMode(mode_info.clone(), client_id))
@ -511,12 +578,6 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
Event::ModeUpdate(mode_info),
)]))
.unwrap();
send_to_client!(
client_id,
os_input,
ServerToClientMsg::SwitchToMode(mode),
session_state
);
},
ServerInstruction::UnblockInputThread => {
let client_ids = session_state.read().unwrap().client_ids();
@ -827,6 +888,42 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.unwrap()
.associate_pipe_with_client(pipe_id, client_id);
},
ServerInstruction::ChangeMode(client_id, input_mode) => {
session_data
.write()
.unwrap()
.as_mut()
.unwrap()
.client_input_modes
.insert(client_id, input_mode);
},
ServerInstruction::ChangeModeForAllClients(input_mode) => {
session_data
.write()
.unwrap()
.as_mut()
.unwrap()
.change_mode_for_all_clients(input_mode);
},
ServerInstruction::RebindKeys(client_id, new_keybinds) => {
let new_keybinds = session_data
.write()
.unwrap()
.as_mut()
.unwrap()
.rebind_keys(client_id, new_keybinds)
.clone();
if let Some(new_keybinds) = new_keybinds {
session_data
.write()
.unwrap()
.as_ref()
.unwrap()
.senders
.send_to_screen(ScreenInstruction::RebindKeys(new_keybinds, client_id))
.unwrap();
}
},
}
}
@ -1054,6 +1151,8 @@ fn init_session(
client_attributes,
layout,
config_options: config_options.clone(),
client_keybinds: HashMap::new(),
client_input_modes: HashMap::new(),
screen_thread: Some(screen_thread),
pty_thread: Some(pty_thread),
plugin_thread: Some(plugin_thread),

View File

@ -6485,6 +6485,87 @@ pub fn disconnect_other_clients_plugins_command() {
assert_snapshot!(format!("{:#?}", switch_session_event));
}
#[test]
#[ignore]
pub fn rebind_keys_plugin_command() {
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
// destructor removes the directory
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, server_receiver, screen_receiver, teardown) =
create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
_allow_exec_host_cmd: false,
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
configuration: Default::default(),
..Default::default()
});
let tab_index = 1;
let client_id = 1;
let size = Size {
cols: 121,
rows: 20,
};
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
received_screen_instructions,
ScreenInstruction::Exit,
screen_receiver,
1,
&PermissionType::ChangeApplicationState,
cache_path,
plugin_thread_sender,
client_id
);
let received_server_instruction = Arc::new(Mutex::new(vec![]));
let server_thread = log_actions_in_thread!(
received_server_instruction,
ServerInstruction::RebindKeys,
server_receiver,
1
);
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
let _ = plugin_thread_sender.send(PluginInstruction::Load(
plugin_should_float,
false,
plugin_title,
run_plugin,
tab_index,
None,
client_id,
size,
None,
false,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
Event::Key(KeyWithModifier::new(BareKey::Char('0')).with_ctrl_modifier()), // this triggers the enent in the fixture plugin
)]));
std::thread::sleep(std::time::Duration::from_millis(500));
teardown();
server_thread.join().unwrap(); // this might take a while if the cache is cold
let rebind_keys_event = received_server_instruction
.lock()
.unwrap()
.iter()
.rev()
.find_map(|i| {
if let ServerInstruction::RebindKeys(..) = i {
Some(i.clone())
} else {
None
}
})
.clone();
assert_snapshot!(format!("{:#?}", rebind_keys_event));
}
#[test]
#[ignore]
pub fn run_plugin_in_specific_cwd() {

View File

@ -1,6 +1,6 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5307
assertion_line: 5500
expression: "format!(\"{:#?}\", permissions)"
---
Some(
@ -14,5 +14,6 @@ Some(
WebAccess,
ReadCliPipes,
MessageAndLaunchOtherPlugins,
RebindKeys,
],
)

View File

@ -0,0 +1,11 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 6566
expression: "format!(\"{:#?}\", rebind_keys_event)"
---
Some(
RebindKeys(
1,
"\n keybinds {\n locked {\n bind \"a\" { NewTab; }\n }\n }\n ",
),
)

View File

@ -1,6 +1,6 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5217
assertion_line: 5409
expression: "format!(\"{:#?}\", new_tab_event)"
---
Some(
@ -16,5 +16,6 @@ Some(
WebAccess,
ReadCliPipes,
MessageAndLaunchOtherPlugins,
RebindKeys,
],
)

View File

@ -268,6 +268,7 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
PluginCommand::WatchFilesystem => watch_filesystem(env),
PluginCommand::DumpSessionLayout => dump_session_layout(env),
PluginCommand::CloseSelf => close_self(env),
PluginCommand::RebindKeys(new_keybinds) => rebind_keys(env, new_keybinds)?,
},
(PermissionStatus::Denied, permission) => {
log::error!(
@ -842,6 +843,16 @@ fn close_self(env: &ForeignFunctionEnv) {
.non_fatal();
}
fn rebind_keys(env: &ForeignFunctionEnv, new_keybinds: String) -> Result<()> {
let err_context = || "Failed to rebind keys";
let client_id = env.plugin_env.client_id;
env.plugin_env
.senders
.send_to_server(ServerInstruction::RebindKeys(client_id, new_keybinds))
.with_context(err_context)?;
Ok(())
}
fn switch_to_mode(env: &ForeignFunctionEnv, input_mode: InputMode) {
let action = Action::SwitchToMode(input_mode);
let error_msg = || {
@ -1594,6 +1605,7 @@ fn check_command_permission(
| PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes,
PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins,
PluginCommand::DumpSessionLayout => PermissionType::ReadApplicationState,
PluginCommand::RebindKeys(..) => PermissionType::RebindKeys,
_ => return (PermissionStatus::Granted, None),
};

View File

@ -101,6 +101,9 @@ pub(crate) fn route_action(
Event::ModeUpdate(get_mode_info(mode, attrs, capabilities)),
)]))
.with_context(err_context)?;
senders
.send_to_server(ServerInstruction::ChangeMode(client_id, mode))
.with_context(err_context)?;
senders
.send_to_screen(ScreenInstruction::ChangeMode(
get_mode_info(mode, attrs, capabilities),
@ -344,6 +347,11 @@ pub(crate) fn route_action(
Event::ModeUpdate(get_mode_info(input_mode, attrs, capabilities)),
)]))
.with_context(err_context)?;
senders
.send_to_server(ServerInstruction::ChangeModeForAllClients(input_mode))
.with_context(err_context)?;
senders
.send_to_screen(ScreenInstruction::ChangeModeForAllClients(get_mode_info(
input_mode,
@ -988,20 +996,42 @@ pub(crate) fn route_thread_main(
-> Result<bool> {
let mut should_break = false;
match instruction {
ClientToServerMsg::Key(key, raw_bytes, is_kitty_keyboard_protocol) => {
if let Some(rlocked_sessions) = rlocked_sessions.as_ref() {
match rlocked_sessions.get_client_keybinds_and_mode(&client_id) {
Some((keybinds, input_mode)) => {
for action in keybinds
.get_actions_for_key_in_mode_or_default_action(
&input_mode,
&key,
raw_bytes,
is_kitty_keyboard_protocol,
)
{
if route_action(
action,
client_id,
None,
rlocked_sessions.senders.clone(),
rlocked_sessions.capabilities.clone(),
rlocked_sessions.client_attributes.clone(),
rlocked_sessions.default_shell.clone(),
rlocked_sessions.layout.clone(),
Some(&mut seen_cli_pipes),
)? {
should_break = true;
}
}
},
None => {
log::error!("Failed to get keybindings for client");
},
}
}
},
ClientToServerMsg::Action(action, maybe_pane_id, maybe_client_id) => {
let client_id = maybe_client_id.unwrap_or(client_id);
if let Some(rlocked_sessions) = rlocked_sessions.as_ref() {
if let Action::SwitchToMode(input_mode) = action {
let send_res = os_input.send_to_client(
client_id,
ServerToClientMsg::SwitchToMode(input_mode),
);
if send_res.is_err() {
let _ = to_server
.send(ServerInstruction::RemoveClient(client_id));
return Ok(true);
}
}
if route_action(
action,
client_id,

View File

@ -13,6 +13,7 @@ use zellij_utils::data::{
};
use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand;
use zellij_utils::input::keybinds::Keybinds;
use zellij_utils::input::options::Clipboard;
use zellij_utils::pane_size::{Size, SizeInPixels};
use zellij_utils::{
@ -360,6 +361,7 @@ pub enum ScreenInstruction {
DumpLayoutToHd,
RenameSession(String, ClientId), // String -> new name
ListClientsMetadata(Option<PathBuf>, ClientId), // Option<PathBuf> - default shell
RebindKeys(Keybinds, ClientId),
}
impl From<&ScreenInstruction> for ScreenContext {
@ -544,6 +546,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::DumpLayoutToHd => ScreenContext::DumpLayoutToHd,
ScreenInstruction::RenameSession(..) => ScreenContext::RenameSession,
ScreenInstruction::ListClientsMetadata(..) => ScreenContext::ListClientsMetadata,
ScreenInstruction::RebindKeys(..) => ScreenContext::RebindKeys,
}
}
}
@ -1774,12 +1777,6 @@ impl Screen {
tab.mark_active_pane_for_rerender(client_id);
tab.update_input_modes()?;
}
if let Some(os_input) = &mut self.bus.os_input {
let _ =
os_input.send_to_client(client_id, ServerToClientMsg::SwitchToMode(mode_info.mode));
}
Ok(())
}
pub fn change_mode_for_all_clients(&mut self, mode_info: ModeInfo) -> Result<()> {
@ -2154,6 +2151,23 @@ impl Screen {
}
Ok(())
}
pub fn rebind_keys(&mut self, new_keybinds: Keybinds, client_id: ClientId) -> Result<()> {
if self.connected_clients_contains(&client_id) {
let mode_info = self
.mode_info
.entry(client_id)
.or_insert_with(|| self.default_mode_info.clone());
mode_info.update_keybinds(new_keybinds);
for tab in self.tabs.values_mut() {
tab.change_mode_info(mode_info.clone(), client_id);
tab.mark_active_pane_for_rerender(client_id);
tab.update_input_modes()?;
}
} else {
log::error!("Could not find client_id {client_id} to rebind keys");
}
Ok(())
}
fn unblock_input(&self) -> Result<()> {
self.bus
.senders
@ -2295,6 +2309,9 @@ impl Screen {
}
found_plugin
}
fn connected_clients_contains(&self, client_id: &ClientId) -> bool {
self.connected_clients.borrow().contains(client_id)
}
}
// The box is here in order to make the
@ -4010,6 +4027,9 @@ pub(crate) fn screen_thread_main(
}
screen.unblock_input()?;
},
ScreenInstruction::RebindKeys(new_keybinds, client_id) => {
screen.rebind_keys(new_keybinds, client_id).non_fatal();
},
}
}
Ok(())

View File

@ -514,6 +514,8 @@ impl MockScreen {
background_jobs_thread: None,
config_options: Default::default(),
layout,
client_input_modes: HashMap::new(),
client_keybinds: HashMap::new(),
}
}
}
@ -571,6 +573,8 @@ impl MockScreen {
background_jobs_thread: None,
config_options: Default::default(),
layout,
client_input_modes: HashMap::new(),
client_keybinds: HashMap::new(),
};
let os_input = FakeInputOutput::default();
@ -2449,31 +2453,41 @@ pub fn send_cli_edit_action_with_split_direction() {
#[test]
pub fn send_cli_switch_mode_action() {
let size = Size {
cols: 121,
rows: 20,
};
let size = Size { cols: 80, rows: 10 };
let client_id = 10; // fake client id should not appear in the screen's state
let mut mock_screen = MockScreen::new(size);
let session_metadata = mock_screen.clone_session_metadata();
let mut initial_layout = TiledPaneLayout::default();
initial_layout.children_split_direction = SplitDirection::Vertical;
initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()];
let mut mock_screen = MockScreen::new(size);
let session_metadata = mock_screen.clone_session_metadata();
let screen_thread = mock_screen.run(Some(initial_layout), vec![]);
let received_server_instructions = Arc::new(Mutex::new(vec![]));
let server_receiver = mock_screen.server_receiver.take().unwrap();
let server_instruction = log_actions_in_thread!(
received_server_instructions,
ServerInstruction::KillSession,
server_receiver
);
let cli_switch_mode = CliAction::SwitchMode {
input_mode: InputMode::Locked,
};
send_cli_action_to_server(&session_metadata, cli_switch_mode, client_id);
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
mock_screen.teardown(vec![screen_thread]);
assert_snapshot!(format!(
"{:?}",
*mock_screen
.os_input
.server_to_client_messages
std::thread::sleep(std::time::Duration::from_millis(100));
mock_screen.teardown(vec![server_instruction, screen_thread]);
let switch_mode_action = received_server_instructions
.lock()
.unwrap()
));
.iter()
.find(|instruction| match instruction {
ServerInstruction::ChangeModeForAllClients(..) => true,
_ => false,
})
.cloned();
assert_snapshot!(format!("{:?}", switch_mode_action));
}
#[test]

View File

@ -1,6 +1,6 @@
---
source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 2465
expression: "format!(\"{:?}\", *\n mock_screen.os_input.server_to_client_messages.lock().unwrap())"
assertion_line: 2521
expression: "format!(\"{:?}\", switch_mode_action)"
---
{1: [QueryTerminalSize, SwitchToMode(Locked)]}
Some(ChangeModeForAllClients(Locked))

View File

@ -808,6 +808,14 @@ pub fn dump_session_layout() {
unsafe { host_run_plugin_command() };
}
/// Rebind keys for the current user
pub fn rebind_keys(keys: String) {
let plugin_command = PluginCommand::RebindKeys(keys);
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
// Utility Functions
#[allow(unused)]

View File

@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32,
#[prost(
oneof = "plugin_command::Payload",
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62"
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63"
)]
pub payload: ::core::option::Option<plugin_command::Payload>,
}
@ -118,6 +118,8 @@ pub mod plugin_command {
ScanHostFolderPayload(::prost::alloc::string::String),
#[prost(message, tag = "62")]
NewTabsWithLayoutInfoPayload(super::NewTabsWithLayoutInfoPayload),
#[prost(string, tag = "63")]
RebindKeysPayload(::prost::alloc::string::String),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
@ -431,6 +433,7 @@ pub enum CommandName {
DumpSessionLayout = 84,
CloseSelf = 85,
NewTabsWithLayoutInfo = 86,
RebindKeys = 87,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@ -526,6 +529,7 @@ impl CommandName {
CommandName::DumpSessionLayout => "DumpSessionLayout",
CommandName::CloseSelf => "CloseSelf",
CommandName::NewTabsWithLayoutInfo => "NewTabsWithLayoutInfo",
CommandName::RebindKeys => "RebindKeys",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@ -618,6 +622,7 @@ impl CommandName {
"DumpSessionLayout" => Some(Self::DumpSessionLayout),
"CloseSelf" => Some(Self::CloseSelf),
"NewTabsWithLayoutInfo" => Some(Self::NewTabsWithLayoutInfo),
"RebindKeys" => Some(Self::RebindKeys),
_ => None,
}
}

View File

@ -10,6 +10,7 @@ pub enum PermissionType {
WebAccess = 6,
ReadCliPipes = 7,
MessageAndLaunchOtherPlugins = 8,
RebindKeys = 9,
}
impl PermissionType {
/// String value of the enum field names used in the ProtoBuf definition.
@ -29,6 +30,7 @@ impl PermissionType {
PermissionType::MessageAndLaunchOtherPlugins => {
"MessageAndLaunchOtherPlugins"
}
PermissionType::RebindKeys => "RebindKeys",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@ -43,6 +45,7 @@ impl PermissionType {
"WebAccess" => Some(Self::WebAccess),
"ReadCliPipes" => Some(Self::ReadCliPipes),
"MessageAndLaunchOtherPlugins" => Some(Self::MessageAndLaunchOtherPlugins),
"RebindKeys" => Some(Self::RebindKeys),
_ => None,
}
}

View File

@ -1,5 +1,6 @@
use crate::input::actions::Action;
use crate::input::config::ConversionError;
use crate::input::keybinds::Keybinds;
use crate::input::layout::SplitSize;
use clap::ArgEnum;
use serde::{Deserialize, Serialize};
@ -914,6 +915,7 @@ pub enum Permission {
WebAccess,
ReadCliPipes,
MessageAndLaunchOtherPlugins,
RebindKeys,
}
impl PermissionType {
@ -934,6 +936,7 @@ impl PermissionType {
PermissionType::MessageAndLaunchOtherPlugins => {
"Send messages to and launch other plugins".to_owned()
},
PermissionType::RebindKeys => "Rebind keys".to_owned(),
}
}
}
@ -1130,6 +1133,9 @@ impl ModeInfo {
}
vec![]
}
pub fn update_keybinds(&mut self, keybinds: Keybinds) {
self.keybinds = keybinds.to_keybinds_vec();
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
@ -1733,4 +1739,5 @@ pub enum PluginCommand {
DumpSessionLayout,
CloseSelf,
NewTabsWithLayoutInfo(LayoutInfo),
RebindKeys(String), // String -> stringified keybindings
}

View File

@ -353,6 +353,7 @@ pub enum ScreenContext {
RenameSession,
DumpLayoutToPlugin,
ListClientsMetadata,
RebindKeys,
}
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
@ -454,6 +455,9 @@ pub enum ServerContext {
CliPipeOutput,
AssociatePipeWithClient,
DisconnectAllClientsExcept,
ChangeMode,
ChangeModeForAllClients,
RebindKeys,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]

View File

@ -1,7 +1,7 @@
//! IPC stuff for starting to split things into a client and server model.
use crate::{
cli::CliArgs,
data::{ClientId, ConnectToSession, InputMode, Style},
data::{ClientId, ConnectToSession, KeyWithModifier, Style},
errors::{get_current_ctx, prelude::*, ErrorContext},
input::keybinds::Keybinds,
input::{actions::Action, layout::Layout, options::Options, plugins::PluginAliases},
@ -85,6 +85,7 @@ pub enum ClientToServerMsg {
Option<(u32, bool)>, // (pane_id, is_plugin) => pane id to focus
),
Action(Action, Option<u32>, Option<ClientId>), // u32 is the terminal id
Key(KeyWithModifier, Vec<u8>, bool), // key, raw_bytes, is_kitty_keyboard_protocol
ClientExited,
KillSession,
ConnStatus,
@ -97,7 +98,6 @@ pub enum ServerToClientMsg {
Render(String),
UnblockInputThread,
Exit(ExitReason),
SwitchToMode(InputMode),
Connected,
ActiveClients(Vec<ClientId>),
Log(Vec<String>),

View File

@ -1880,6 +1880,22 @@ impl Keybinds {
}
Ok(input_mode_keybinds)
}
pub fn from_string(
stringified_keybindings: String,
base_keybinds: Keybinds,
config_options: &Options,
) -> Result<Self, ConfigError> {
let document: KdlDocument = stringified_keybindings.parse()?;
if let Some(kdl_keybinds) = document.get("keybinds") {
Keybinds::from_kdl(&kdl_keybinds, base_keybinds, config_options)
} else {
Err(ConfigError::new_kdl_error(
format!("Could not find keybinds node"),
document.span().offset(),
document.span().len(),
))
}
}
}
impl Config {

View File

@ -98,6 +98,7 @@ enum CommandName {
DumpSessionLayout = 84;
CloseSelf = 85;
NewTabsWithLayoutInfo = 86;
RebindKeys = 87;
}
message PluginCommand {
@ -155,6 +156,7 @@ message PluginCommand {
KillSessionsPayload kill_sessions_payload = 60;
string scan_host_folder_payload = 61;
NewTabsWithLayoutInfoPayload new_tabs_with_layout_info_payload = 62;
string rebind_keys_payload = 63;
}
}

View File

@ -888,6 +888,12 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
},
_ => Err("Mismatched payload for NewTabsWithLayoutInfo"),
},
Some(CommandName::RebindKeys) => match protobuf_plugin_command.payload {
Some(Payload::RebindKeysPayload(rebind_keys_payload)) => {
Ok(PluginCommand::RebindKeys(rebind_keys_payload))
},
_ => Err("Mismatched payload for RebindKeys"),
},
None => Err("Unrecognized plugin command"),
}
}
@ -1420,6 +1426,10 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
)),
})
},
PluginCommand::RebindKeys(rebind_keys_payload) => Ok(ProtobufPluginCommand {
name: CommandName::RebindKeys as i32,
payload: Some(Payload::RebindKeysPayload(rebind_keys_payload)),
}),
}
}
}

View File

@ -12,4 +12,5 @@ enum PermissionType {
WebAccess = 6;
ReadCliPipes = 7;
MessageAndLaunchOtherPlugins = 8;
RebindKeys = 9;
}

View File

@ -24,6 +24,7 @@ impl TryFrom<ProtobufPermissionType> for PermissionType {
ProtobufPermissionType::MessageAndLaunchOtherPlugins => {
Ok(PermissionType::MessageAndLaunchOtherPlugins)
},
ProtobufPermissionType::RebindKeys => Ok(PermissionType::RebindKeys),
}
}
}
@ -49,6 +50,7 @@ impl TryFrom<PermissionType> for ProtobufPermissionType {
PermissionType::MessageAndLaunchOtherPlugins => {
Ok(ProtobufPermissionType::MessageAndLaunchOtherPlugins)
},
PermissionType::RebindKeys => Ok(ProtobufPermissionType::RebindKeys),
}
}
}