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::WebAccess,
PermissionType::ReadCliPipes, PermissionType::ReadCliPipes,
PermissionType::MessageAndLaunchOtherPlugins, PermissionType::MessageAndLaunchOtherPlugins,
PermissionType::RebindKeys,
]); ]);
self.configuration = configuration; self.configuration = configuration;
subscribe(&[ subscribe(&[
@ -318,6 +319,18 @@ impl ZellijPlugin for State {
Some(std::path::PathBuf::from("/tmp")), 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) => { 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) if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
// back to normal mode after split // back to normal mode after split
step_is_complete = true; 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; let mut step_is_complete = false;
if remote_terminal.cursor_position_is(3, 2) { if remote_terminal.cursor_position_is(3, 2) {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
// back to normal mode after split // back to normal mode after split
step_is_complete = true; step_is_complete = true;
@ -259,6 +261,7 @@ pub fn scrolling_inside_a_pane() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -286,6 +289,7 @@ pub fn scrolling_inside_a_pane() {
{ {
// all lines have been written to the pane // all lines have been written to the pane
remote_terminal.send_key(&SCROLL_MODE); remote_terminal.send_key(&SCROLL_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SCROLL_UP_IN_SCROLL_MODE); remote_terminal.send_key(&SCROLL_UP_IN_SCROLL_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -337,6 +341,7 @@ pub fn toggle_pane_fullscreen() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; 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() { if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane // cursor is in the newly opened second pane
remote_terminal.send_key(&PANE_MODE); 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); remote_terminal.send_key(&TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -398,6 +404,7 @@ pub fn open_new_tab() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; 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() { if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane // cursor is in the newly opened second pane
remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -463,6 +471,7 @@ pub fn close_tab() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -476,6 +485,7 @@ pub fn close_tab() {
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane // cursor is in the newly opened second pane
remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -493,6 +503,7 @@ pub fn close_tab() {
{ {
// cursor is in the newly opened second tab // cursor is in the newly opened second tab
remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&CLOSE_TAB_IN_TAB_MODE); remote_terminal.send_key(&CLOSE_TAB_IN_TAB_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -653,6 +664,7 @@ pub fn close_pane() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -666,6 +678,7 @@ pub fn close_pane() {
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane // cursor is in the newly opened second pane
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&CLOSE_PANE_IN_PANE_MODE); remote_terminal.send_key(&CLOSE_PANE_IN_PANE_MODE);
step_is_complete = true; 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) if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&CLOSE_PANE_IN_PANE_MODE); remote_terminal.send_key(&CLOSE_PANE_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -795,6 +809,7 @@ pub fn typing_exit_closes_pane() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -807,9 +822,13 @@ pub fn typing_exit_closes_pane() {
let mut step_is_complete = false; let mut step_is_complete = false;
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
remote_terminal.send_key("e".as_bytes()); remote_terminal.send_key("e".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("x".as_bytes()); remote_terminal.send_key("x".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("i".as_bytes()); remote_terminal.send_key("i".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("t".as_bytes()); remote_terminal.send_key("t".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("\n".as_bytes()); remote_terminal.send_key("\n".as_bytes());
step_is_complete = true; step_is_complete = true;
} }
@ -859,6 +878,7 @@ pub fn resize_pane() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -872,6 +892,7 @@ pub fn resize_pane() {
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane // cursor is in the newly opened second pane
remote_terminal.send_key(&RESIZE_MODE); remote_terminal.send_key(&RESIZE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&RESIZE_LEFT_IN_RESIZE_MODE); remote_terminal.send_key(&RESIZE_LEFT_IN_RESIZE_MODE);
// back to normal mode // back to normal mode
remote_terminal.send_key(&ENTER); remote_terminal.send_key(&ENTER);
@ -933,7 +954,9 @@ pub fn lock_mode() {
let mut step_is_complete = false; let mut step_is_complete = false;
if remote_terminal.snapshot_contains("INTERFACE LOCKED") { if remote_terminal.snapshot_contains("INTERFACE LOCKED") {
remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); 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()); remote_terminal.send_key("abc".as_bytes());
step_is_complete = true; step_is_complete = true;
} }
@ -983,6 +1006,7 @@ pub fn resize_terminal_window() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -1046,6 +1070,7 @@ pub fn detach_and_attach_session() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -1070,6 +1095,7 @@ pub fn detach_and_attach_session() {
let mut step_is_complete = false; let mut step_is_complete = false;
if remote_terminal.cursor_position_is(77, 2) { if remote_terminal.cursor_position_is(77, 2) {
remote_terminal.send_key(&SESSION_MODE); remote_terminal.send_key(&SESSION_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&DETACH_IN_SESSION_MODE); remote_terminal.send_key(&DETACH_IN_SESSION_MODE);
// text has been entered // text has been entered
step_is_complete = true; step_is_complete = true;
@ -1286,6 +1312,7 @@ fn focus_pane_with_mouse() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; 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.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; 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) if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(2, 1)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -1489,6 +1518,7 @@ pub fn mirrored_sessions() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -1502,6 +1532,7 @@ pub fn mirrored_sessions() {
if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() { if remote_terminal.cursor_position_is(63, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane // cursor is in the newly opened second pane
remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
step_is_complete = true; step_is_complete = true;
} }
@ -1544,6 +1575,7 @@ pub fn mirrored_sessions() {
let mut step_is_complete = false; let mut step_is_complete = false;
if remote_terminal.snapshot_contains("some text") { if remote_terminal.snapshot_contains("some text") {
remote_terminal.send_key(&TAB_MODE); 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 remote_terminal.send_key(&MOVE_FOCUS_LEFT_IN_PANE_MODE); // same key as tab mode
step_is_complete = true; 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.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE);
step_is_complete = true; 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() { if remote_terminal.cursor_position_is(3, 2) && remote_terminal.tip_appears() {
// cursor is in the newly opened second pane // cursor is in the newly opened second pane
remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE);
step_is_complete = true; 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 #1 [ ]")
&& remote_terminal.snapshot_contains("Tab #2") && remote_terminal.snapshot_contains("Tab #2")
&& remote_terminal.status_bar_appears() && remote_terminal.status_bar_appears()
&& !remote_terminal.snapshot_contains("AND:")
{ {
// cursor is in the newly opened second tab // cursor is in the newly opened second tab
step_is_complete = true; step_is_complete = true;
@ -1899,11 +1934,17 @@ pub fn bracketed_paste() {
&& remote_terminal.cursor_position_is(3, 2) && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&BRACKETED_PASTE_START); remote_terminal.send_key(&BRACKETED_PASTE_START);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); 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()); remote_terminal.send_key("a".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("b".as_bytes()); remote_terminal.send_key("b".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("c".as_bytes()); remote_terminal.send_key("c".as_bytes());
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&BRACKETED_PASTE_END); remote_terminal.send_key(&BRACKETED_PASTE_END);
step_is_complete = true; 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) if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&TOGGLE_FLOATING_PANES); remote_terminal.send_key(&TOGGLE_FLOATING_PANES);
// back to normal mode after split // back to normal mode after split
step_is_complete = true; 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) if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&TMUX_MODE); remote_terminal.send_key(&TMUX_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPLIT_RIGHT_IN_TMUX_MODE); remote_terminal.send_key(&SPLIT_RIGHT_IN_TMUX_MODE);
// back to normal mode after split // back to normal mode after split
step_is_complete = true; 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) if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&SCROLL_MODE); remote_terminal.send_key(&SCROLL_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&EDIT_SCROLLBACK); remote_terminal.send_key(&EDIT_SCROLLBACK);
step_is_complete = true; step_is_complete = true;
} }
@ -2099,8 +2143,13 @@ pub fn undo_rename_tab() {
&& remote_terminal.snapshot_contains("Tab #1") && remote_terminal.snapshot_contains("Tab #1")
{ {
remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&RENAME_TAB_MODE); remote_terminal.send_key(&RENAME_TAB_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&[97, 97]); 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); remote_terminal.send_key(&ESC);
step_is_complete = true; step_is_complete = true;
} }
@ -2113,7 +2162,9 @@ pub fn undo_rename_tab() {
name: "Wait for tab name to apper on screen", name: "Wait for tab name to apper on screen",
instruction: |remote_terminal: RemoteTerminal| -> bool { instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false; 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 = true
} }
step_is_complete 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) if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2)
{ {
remote_terminal.send_key(&PANE_MODE); remote_terminal.send_key(&PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&RENAME_PANE_MODE); remote_terminal.send_key(&RENAME_PANE_MODE);
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&[97, 97]); 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); remote_terminal.send_key(&ESC);
step_is_complete = true; step_is_complete = true;
} }
@ -2163,7 +2219,9 @@ pub fn undo_rename_pane() {
name: "Wait for pane name to apper on screen", name: "Wait for pane name to apper on screen",
instruction: |remote_terminal: RemoteTerminal| -> bool { instruction: |remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false; 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 = true
} }
step_is_complete step_is_complete
@ -2212,6 +2270,7 @@ pub fn send_command_through_the_cli() {
"{}/append-echo-script.sh", "{}/append-echo-script.sh",
fixture_folder fixture_folder
)); ));
std::thread::sleep(std::time::Duration::from_millis(100));
step_is_complete = true; step_is_complete = true;
} }
step_is_complete step_is_complete
@ -2221,11 +2280,7 @@ pub fn send_command_through_the_cli() {
name: "Initial run of suspended command", name: "Initial run of suspended command",
instruction: |mut remote_terminal: RemoteTerminal| -> bool { instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false; let mut step_is_complete = false;
if remote_terminal.snapshot_contains("<Ctrl-c>") if remote_terminal.snapshot_contains("<Ctrl-c>") {
&& remote_terminal.cursor_position_is(0, 0)
// cursor does not appear in
// suspend_start panes
{
std::thread::sleep(std::time::Duration::from_millis(100)); std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key(&SPACE); // run script - here we use SPACE remote_terminal.send_key(&SPACE); // run script - here we use SPACE
// instead of the default ENTER because // instead of the default ENTER because

View file

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

View file

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

View file

@ -146,9 +146,6 @@ impl InputHandler {
)) => { )) => {
self.handle_key(&key_with_modifier, raw_bytes, true); self.handle_key(&key_with_modifier, raw_bytes, true);
}, },
Ok((InputInstruction::SwitchToMode(input_mode), _error_context)) => {
self.mode = input_mode;
},
Ok(( Ok((
InputInstruction::AnsiStdinInstructions(ansi_stdin_instructions), InputInstruction::AnsiStdinInstructions(ansi_stdin_instructions),
_error_context, _error_context,
@ -180,18 +177,13 @@ impl InputHandler {
raw_bytes: Vec<u8>, raw_bytes: Vec<u8>,
is_kitty_keyboard_protocol: bool, is_kitty_keyboard_protocol: bool,
) { ) {
let keybinds = &self.config.keybinds; // we interpret the keys into actions on the server side so that we can change the
for action in keybinds.get_actions_for_key_in_mode_or_default_action( // keybinds at runtime
&self.mode, self.os_input.send_to_server(ClientToServerMsg::Key(
key, key.clone(),
raw_bytes, raw_bytes,
is_kitty_keyboard_protocol, 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) { fn handle_stdin_ansi_instruction(&mut self, ansi_stdin_instructions: AnsiStdinInstruction) {
match ansi_stdin_instructions { match ansi_stdin_instructions {
@ -316,14 +308,8 @@ impl InputHandler {
self.exit(ExitReason::NormalDetached); self.exit(ExitReason::NormalDetached);
should_break = true; 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::CloseFocus
| Action::SwitchToMode(..)
| Action::ClearScreen | Action::ClearScreen
| Action::NewPane(..) | Action::NewPane(..)
| Action::Run(_) | Action::Run(_)

View file

@ -25,7 +25,7 @@ use crate::{
use zellij_utils::{ use zellij_utils::{
channels::{self, ChannelWithContext, SenderWithContext}, channels::{self, ChannelWithContext, SenderWithContext},
consts::{set_permissions, ZELLIJ_SOCK_DIR}, consts::{set_permissions, ZELLIJ_SOCK_DIR},
data::{ClientId, ConnectToSession, InputMode, KeyWithModifier, Style}, data::{ClientId, ConnectToSession, KeyWithModifier, Style},
envs, envs,
errors::{ClientContext, ContextType, ErrorInstruction}, errors::{ClientContext, ContextType, ErrorInstruction},
input::{config::Config, options::Options}, input::{config::Config, options::Options},
@ -42,7 +42,6 @@ pub(crate) enum ClientInstruction {
Render(String), Render(String),
UnblockInputThread, UnblockInputThread,
Exit(ExitReason), Exit(ExitReason),
SwitchToMode(InputMode),
Connected, Connected,
ActiveClients(Vec<ClientId>), ActiveClients(Vec<ClientId>),
StartedParsingStdinQuery, StartedParsingStdinQuery,
@ -62,9 +61,6 @@ impl From<ServerToClientMsg> for ClientInstruction {
ServerToClientMsg::Exit(e) => ClientInstruction::Exit(e), ServerToClientMsg::Exit(e) => ClientInstruction::Exit(e),
ServerToClientMsg::Render(buffer) => ClientInstruction::Render(buffer), ServerToClientMsg::Render(buffer) => ClientInstruction::Render(buffer),
ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread, ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread,
ServerToClientMsg::SwitchToMode(input_mode) => {
ClientInstruction::SwitchToMode(input_mode)
},
ServerToClientMsg::Connected => ClientInstruction::Connected, ServerToClientMsg::Connected => ClientInstruction::Connected,
ServerToClientMsg::ActiveClients(clients) => ClientInstruction::ActiveClients(clients), ServerToClientMsg::ActiveClients(clients) => ClientInstruction::ActiveClients(clients),
ServerToClientMsg::Log(log_lines) => ClientInstruction::Log(log_lines), ServerToClientMsg::Log(log_lines) => ClientInstruction::Log(log_lines),
@ -90,7 +86,6 @@ impl From<&ClientInstruction> for ClientContext {
ClientInstruction::Error(_) => ClientContext::Error, ClientInstruction::Error(_) => ClientContext::Error,
ClientInstruction::Render(_) => ClientContext::Render, ClientInstruction::Render(_) => ClientContext::Render,
ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread, ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread,
ClientInstruction::SwitchToMode(_) => ClientContext::SwitchToMode,
ClientInstruction::Connected => ClientContext::Connected, ClientInstruction::Connected => ClientContext::Connected,
ClientInstruction::ActiveClients(_) => ClientContext::ActiveClients, ClientInstruction::ActiveClients(_) => ClientContext::ActiveClients,
ClientInstruction::Log(_) => ClientContext::Log, ClientInstruction::Log(_) => ClientContext::Log,
@ -154,7 +149,6 @@ impl ClientInfo {
pub(crate) enum InputInstruction { pub(crate) enum InputInstruction {
KeyEvent(InputEvent, Vec<u8>), KeyEvent(InputEvent, Vec<u8>),
KeyWithModifierEvent(KeyWithModifier, Vec<u8>), KeyWithModifierEvent(KeyWithModifier, Vec<u8>),
SwitchToMode(InputMode),
AnsiStdinInstructions(Vec<AnsiStdinInstruction>), AnsiStdinInstructions(Vec<AnsiStdinInstruction>),
StartedParsing, StartedParsing,
DoneParsing, DoneParsing,
@ -505,11 +499,6 @@ pub fn start_client(
ClientInstruction::UnblockInputThread => { ClientInstruction::UnblockInputThread => {
command_is_executing.unblock_input_thread(); command_is_executing.unblock_input_thread();
}, },
ClientInstruction::SwitchToMode(input_mode) => {
send_input_instructions
.send(InputInstruction::SwitchToMode(input_mode))
.unwrap();
},
ClientInstruction::Log(lines_to_log) => { ClientInstruction::Log(lines_to_log) => {
for line in lines_to_log { for line in lines_to_log {
log::info!("{line}"); log::info!("{line}");
@ -634,7 +623,3 @@ pub fn start_server_detached(
os_input.connect_to_server(&*ipc_pipe); os_input.connect_to_server(&*ipc_pipe);
os_input.send_to_server(first_msg); 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}, channels::{self, ChannelWithContext, SenderWithContext},
cli::CliArgs, cli::CliArgs,
consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE}, consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE},
data::{ConnectToSession, Event, PluginCapabilities}, data::{ConnectToSession, Event, InputMode, PluginCapabilities},
errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext}, errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext},
home::{default_layout_dir, get_default_data_dir}, home::{default_layout_dir, get_default_data_dir},
input::{ input::{
command::{RunCommand, TerminalAction}, command::{RunCommand, TerminalAction},
get_mode_info, get_mode_info,
keybinds::Keybinds,
layout::Layout, layout::Layout,
options::Options, options::Options,
plugins::PluginAliases, plugins::PluginAliases,
@ -94,6 +95,9 @@ pub enum ServerInstruction {
client_id: ClientId, client_id: ClientId,
}, },
DisconnectAllClientsExcept(ClientId), DisconnectAllClientsExcept(ClientId),
ChangeMode(ClientId, InputMode),
ChangeModeForAllClients(InputMode),
RebindKeys(ClientId, String), // String -> stringified keybindings
} }
impl From<&ServerInstruction> for ServerContext { impl From<&ServerInstruction> for ServerContext {
@ -121,6 +125,11 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::DisconnectAllClientsExcept(..) => { ServerInstruction::DisconnectAllClientsExcept(..) => {
ServerContext::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 default_shell: Option<TerminalAction>,
pub layout: Box<Layout>, pub layout: Box<Layout>,
pub config_options: Box<Options>, pub config_options: Box<Options>,
pub client_keybinds: HashMap<ClientId, Keybinds>,
pub client_input_modes: HashMap<ClientId, InputMode>,
screen_thread: Option<thread::JoinHandle<()>>, screen_thread: Option<thread::JoinHandle<()>>,
pty_thread: Option<thread::JoinHandle<()>>, pty_thread: Option<thread::JoinHandle<()>>,
plugin_thread: Option<thread::JoinHandle<()>>, plugin_thread: Option<thread::JoinHandle<()>>,
@ -145,6 +156,56 @@ pub(crate) struct SessionMetaData {
background_jobs_thread: Option<thread::JoinHandle<()>>, 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 { impl Drop for SessionMetaData {
fn drop(&mut self) { fn drop(&mut self) {
let _ = self.senders.send_to_pty(PtyInstruction::Exit); 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, plugin_aliases,
); );
*session_data.write().unwrap() = Some(session); *session_data.write().unwrap() = Some(session);
session_data
.write()
.unwrap()
.as_mut()
.unwrap()
.set_client_keybinds(client_id, client_attributes.keybinds.clone());
session_state session_state
.write() .write()
.unwrap() .unwrap()
@ -469,8 +536,9 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
pane_id_to_focus, pane_id_to_focus,
client_id, client_id,
) => { ) => {
let rlock = session_data.read().unwrap(); let mut rlock = session_data.write().unwrap();
let session_data = rlock.as_ref().unwrap(); let session_data = rlock.as_mut().unwrap();
session_data.set_client_keybinds(client_id, attrs.keybinds.clone());
session_state session_state
.write() .write()
.unwrap() .unwrap()
@ -498,7 +566,6 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.unwrap(); .unwrap();
let default_mode = options.default_mode.unwrap_or_default(); let default_mode = options.default_mode.unwrap_or_default();
let mode_info = get_mode_info(default_mode, &attrs, session_data.capabilities); let mode_info = get_mode_info(default_mode, &attrs, session_data.capabilities);
let mode = mode_info.mode;
session_data session_data
.senders .senders
.send_to_screen(ScreenInstruction::ChangeMode(mode_info.clone(), client_id)) .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), Event::ModeUpdate(mode_info),
)])) )]))
.unwrap(); .unwrap();
send_to_client!(
client_id,
os_input,
ServerToClientMsg::SwitchToMode(mode),
session_state
);
}, },
ServerInstruction::UnblockInputThread => { ServerInstruction::UnblockInputThread => {
let client_ids = session_state.read().unwrap().client_ids(); 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() .unwrap()
.associate_pipe_with_client(pipe_id, client_id); .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, client_attributes,
layout, layout,
config_options: config_options.clone(), config_options: config_options.clone(),
client_keybinds: HashMap::new(),
client_input_modes: HashMap::new(),
screen_thread: Some(screen_thread), screen_thread: Some(screen_thread),
pty_thread: Some(pty_thread), pty_thread: Some(pty_thread),
plugin_thread: Some(plugin_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)); 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] #[test]
#[ignore] #[ignore]
pub fn run_plugin_in_specific_cwd() { pub fn run_plugin_in_specific_cwd() {

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/plugins/./unit/plugin_tests.rs source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5307 assertion_line: 5500
expression: "format!(\"{:#?}\", permissions)" expression: "format!(\"{:#?}\", permissions)"
--- ---
Some( Some(
@ -14,5 +14,6 @@ Some(
WebAccess, WebAccess,
ReadCliPipes, ReadCliPipes,
MessageAndLaunchOtherPlugins, 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 source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 5217 assertion_line: 5409
expression: "format!(\"{:#?}\", new_tab_event)" expression: "format!(\"{:#?}\", new_tab_event)"
--- ---
Some( Some(
@ -16,5 +16,6 @@ Some(
WebAccess, WebAccess,
ReadCliPipes, ReadCliPipes,
MessageAndLaunchOtherPlugins, MessageAndLaunchOtherPlugins,
RebindKeys,
], ],
) )

View file

@ -268,6 +268,7 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
PluginCommand::WatchFilesystem => watch_filesystem(env), PluginCommand::WatchFilesystem => watch_filesystem(env),
PluginCommand::DumpSessionLayout => dump_session_layout(env), PluginCommand::DumpSessionLayout => dump_session_layout(env),
PluginCommand::CloseSelf => close_self(env), PluginCommand::CloseSelf => close_self(env),
PluginCommand::RebindKeys(new_keybinds) => rebind_keys(env, new_keybinds)?,
}, },
(PermissionStatus::Denied, permission) => { (PermissionStatus::Denied, permission) => {
log::error!( log::error!(
@ -842,6 +843,16 @@ fn close_self(env: &ForeignFunctionEnv) {
.non_fatal(); .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) { fn switch_to_mode(env: &ForeignFunctionEnv, input_mode: InputMode) {
let action = Action::SwitchToMode(input_mode); let action = Action::SwitchToMode(input_mode);
let error_msg = || { let error_msg = || {
@ -1594,6 +1605,7 @@ fn check_command_permission(
| PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes, | PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes,
PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins, PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins,
PluginCommand::DumpSessionLayout => PermissionType::ReadApplicationState, PluginCommand::DumpSessionLayout => PermissionType::ReadApplicationState,
PluginCommand::RebindKeys(..) => PermissionType::RebindKeys,
_ => return (PermissionStatus::Granted, None), _ => return (PermissionStatus::Granted, None),
}; };

View file

@ -101,6 +101,9 @@ pub(crate) fn route_action(
Event::ModeUpdate(get_mode_info(mode, attrs, capabilities)), Event::ModeUpdate(get_mode_info(mode, attrs, capabilities)),
)])) )]))
.with_context(err_context)?; .with_context(err_context)?;
senders
.send_to_server(ServerInstruction::ChangeMode(client_id, mode))
.with_context(err_context)?;
senders senders
.send_to_screen(ScreenInstruction::ChangeMode( .send_to_screen(ScreenInstruction::ChangeMode(
get_mode_info(mode, attrs, capabilities), get_mode_info(mode, attrs, capabilities),
@ -344,6 +347,11 @@ pub(crate) fn route_action(
Event::ModeUpdate(get_mode_info(input_mode, attrs, capabilities)), Event::ModeUpdate(get_mode_info(input_mode, attrs, capabilities)),
)])) )]))
.with_context(err_context)?; .with_context(err_context)?;
senders
.send_to_server(ServerInstruction::ChangeModeForAllClients(input_mode))
.with_context(err_context)?;
senders senders
.send_to_screen(ScreenInstruction::ChangeModeForAllClients(get_mode_info( .send_to_screen(ScreenInstruction::ChangeModeForAllClients(get_mode_info(
input_mode, input_mode,
@ -988,20 +996,42 @@ pub(crate) fn route_thread_main(
-> Result<bool> { -> Result<bool> {
let mut should_break = false; let mut should_break = false;
match instruction { 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) => { ClientToServerMsg::Action(action, maybe_pane_id, maybe_client_id) => {
let client_id = maybe_client_id.unwrap_or(client_id); let client_id = maybe_client_id.unwrap_or(client_id);
if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { 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( if route_action(
action, action,
client_id, client_id,

View file

@ -13,6 +13,7 @@ use zellij_utils::data::{
}; };
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand; use zellij_utils::input::command::RunCommand;
use zellij_utils::input::keybinds::Keybinds;
use zellij_utils::input::options::Clipboard; use zellij_utils::input::options::Clipboard;
use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::pane_size::{Size, SizeInPixels};
use zellij_utils::{ use zellij_utils::{
@ -360,6 +361,7 @@ pub enum ScreenInstruction {
DumpLayoutToHd, DumpLayoutToHd,
RenameSession(String, ClientId), // String -> new name RenameSession(String, ClientId), // String -> new name
ListClientsMetadata(Option<PathBuf>, ClientId), // Option<PathBuf> - default shell ListClientsMetadata(Option<PathBuf>, ClientId), // Option<PathBuf> - default shell
RebindKeys(Keybinds, ClientId),
} }
impl From<&ScreenInstruction> for ScreenContext { impl From<&ScreenInstruction> for ScreenContext {
@ -544,6 +546,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::DumpLayoutToHd => ScreenContext::DumpLayoutToHd, ScreenInstruction::DumpLayoutToHd => ScreenContext::DumpLayoutToHd,
ScreenInstruction::RenameSession(..) => ScreenContext::RenameSession, ScreenInstruction::RenameSession(..) => ScreenContext::RenameSession,
ScreenInstruction::ListClientsMetadata(..) => ScreenContext::ListClientsMetadata, ScreenInstruction::ListClientsMetadata(..) => ScreenContext::ListClientsMetadata,
ScreenInstruction::RebindKeys(..) => ScreenContext::RebindKeys,
} }
} }
} }
@ -1774,12 +1777,6 @@ impl Screen {
tab.mark_active_pane_for_rerender(client_id); tab.mark_active_pane_for_rerender(client_id);
tab.update_input_modes()?; 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(()) Ok(())
} }
pub fn change_mode_for_all_clients(&mut self, mode_info: ModeInfo) -> Result<()> { pub fn change_mode_for_all_clients(&mut self, mode_info: ModeInfo) -> Result<()> {
@ -2154,6 +2151,23 @@ impl Screen {
} }
Ok(()) 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<()> { fn unblock_input(&self) -> Result<()> {
self.bus self.bus
.senders .senders
@ -2295,6 +2309,9 @@ impl Screen {
} }
found_plugin 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 // The box is here in order to make the
@ -4010,6 +4027,9 @@ pub(crate) fn screen_thread_main(
} }
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::RebindKeys(new_keybinds, client_id) => {
screen.rebind_keys(new_keybinds, client_id).non_fatal();
},
} }
} }
Ok(()) Ok(())

View file

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

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 2465 assertion_line: 2521
expression: "format!(\"{:?}\", *\n mock_screen.os_input.server_to_client_messages.lock().unwrap())" 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() }; 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 // Utility Functions
#[allow(unused)] #[allow(unused)]

View file

@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32, pub name: i32,
#[prost( #[prost(
oneof = "plugin_command::Payload", 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>, pub payload: ::core::option::Option<plugin_command::Payload>,
} }
@ -118,6 +118,8 @@ pub mod plugin_command {
ScanHostFolderPayload(::prost::alloc::string::String), ScanHostFolderPayload(::prost::alloc::string::String),
#[prost(message, tag = "62")] #[prost(message, tag = "62")]
NewTabsWithLayoutInfoPayload(super::NewTabsWithLayoutInfoPayload), NewTabsWithLayoutInfoPayload(super::NewTabsWithLayoutInfoPayload),
#[prost(string, tag = "63")]
RebindKeysPayload(::prost::alloc::string::String),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
@ -431,6 +433,7 @@ pub enum CommandName {
DumpSessionLayout = 84, DumpSessionLayout = 84,
CloseSelf = 85, CloseSelf = 85,
NewTabsWithLayoutInfo = 86, NewTabsWithLayoutInfo = 86,
RebindKeys = 87,
} }
impl CommandName { impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -526,6 +529,7 @@ impl CommandName {
CommandName::DumpSessionLayout => "DumpSessionLayout", CommandName::DumpSessionLayout => "DumpSessionLayout",
CommandName::CloseSelf => "CloseSelf", CommandName::CloseSelf => "CloseSelf",
CommandName::NewTabsWithLayoutInfo => "NewTabsWithLayoutInfo", CommandName::NewTabsWithLayoutInfo => "NewTabsWithLayoutInfo",
CommandName::RebindKeys => "RebindKeys",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -618,6 +622,7 @@ impl CommandName {
"DumpSessionLayout" => Some(Self::DumpSessionLayout), "DumpSessionLayout" => Some(Self::DumpSessionLayout),
"CloseSelf" => Some(Self::CloseSelf), "CloseSelf" => Some(Self::CloseSelf),
"NewTabsWithLayoutInfo" => Some(Self::NewTabsWithLayoutInfo), "NewTabsWithLayoutInfo" => Some(Self::NewTabsWithLayoutInfo),
"RebindKeys" => Some(Self::RebindKeys),
_ => None, _ => None,
} }
} }

View file

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

View file

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

View file

@ -353,6 +353,7 @@ pub enum ScreenContext {
RenameSession, RenameSession,
DumpLayoutToPlugin, DumpLayoutToPlugin,
ListClientsMetadata, ListClientsMetadata,
RebindKeys,
} }
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s. /// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
@ -454,6 +455,9 @@ pub enum ServerContext {
CliPipeOutput, CliPipeOutput,
AssociatePipeWithClient, AssociatePipeWithClient,
DisconnectAllClientsExcept, DisconnectAllClientsExcept,
ChangeMode,
ChangeModeForAllClients,
RebindKeys,
} }
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[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. //! IPC stuff for starting to split things into a client and server model.
use crate::{ use crate::{
cli::CliArgs, cli::CliArgs,
data::{ClientId, ConnectToSession, InputMode, Style}, data::{ClientId, ConnectToSession, KeyWithModifier, Style},
errors::{get_current_ctx, prelude::*, ErrorContext}, errors::{get_current_ctx, prelude::*, ErrorContext},
input::keybinds::Keybinds, input::keybinds::Keybinds,
input::{actions::Action, layout::Layout, options::Options, plugins::PluginAliases}, 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 Option<(u32, bool)>, // (pane_id, is_plugin) => pane id to focus
), ),
Action(Action, Option<u32>, Option<ClientId>), // u32 is the terminal id Action(Action, Option<u32>, Option<ClientId>), // u32 is the terminal id
Key(KeyWithModifier, Vec<u8>, bool), // key, raw_bytes, is_kitty_keyboard_protocol
ClientExited, ClientExited,
KillSession, KillSession,
ConnStatus, ConnStatus,
@ -97,7 +98,6 @@ pub enum ServerToClientMsg {
Render(String), Render(String),
UnblockInputThread, UnblockInputThread,
Exit(ExitReason), Exit(ExitReason),
SwitchToMode(InputMode),
Connected, Connected,
ActiveClients(Vec<ClientId>), ActiveClients(Vec<ClientId>),
Log(Vec<String>), Log(Vec<String>),

View file

@ -1880,6 +1880,22 @@ impl Keybinds {
} }
Ok(input_mode_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 { impl Config {

View file

@ -98,6 +98,7 @@ enum CommandName {
DumpSessionLayout = 84; DumpSessionLayout = 84;
CloseSelf = 85; CloseSelf = 85;
NewTabsWithLayoutInfo = 86; NewTabsWithLayoutInfo = 86;
RebindKeys = 87;
} }
message PluginCommand { message PluginCommand {
@ -155,6 +156,7 @@ message PluginCommand {
KillSessionsPayload kill_sessions_payload = 60; KillSessionsPayload kill_sessions_payload = 60;
string scan_host_folder_payload = 61; string scan_host_folder_payload = 61;
NewTabsWithLayoutInfoPayload new_tabs_with_layout_info_payload = 62; 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"), _ => 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"), 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; WebAccess = 6;
ReadCliPipes = 7; ReadCliPipes = 7;
MessageAndLaunchOtherPlugins = 8; MessageAndLaunchOtherPlugins = 8;
RebindKeys = 9;
} }

View file

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