From 7088d361054c4eb14662cda040bd2e71e60100e9 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 15 Oct 2024 17:27:44 +0200 Subject: [PATCH] feat(plugins): rebind keys api (#3680) * feat(plugins): add API to explicitly unbind/rebind specific keys in specific modes * style(fmt): rustfmt --- .../fixture-plugin-for-tests/src/main.rs | 80 ++++++ zellij-server/src/lib.rs | 97 +++++++- .../src/plugins/unit/plugin_tests.rs | 81 ++++++ ...gin_tests__rebind_keys_plugin_command.snap | 232 +++++++++++++++++- zellij-server/src/plugins/zellij_exports.rs | 32 ++- zellij-tile/src/shim.rs | 19 +- .../assets/prost/api.plugin_command.rs | 35 ++- zellij-utils/src/data.rs | 5 + zellij-utils/src/errors.rs | 1 + .../src/plugin_api/plugin_command.proto | 21 ++ zellij-utils/src/plugin_api/plugin_command.rs | 121 +++++++-- 11 files changed, 697 insertions(+), 27 deletions(-) diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 35f8d574d8..6aca871875 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -445,6 +445,86 @@ impl ZellijPlugin for State { skip_plugin_cache, ) }, + BareKey::Char('y') if key.has_modifiers(&[KeyModifier::Alt]) => { + let write_to_disk = true; + let mut keys_to_unbind = vec![ + ( + InputMode::Locked, + KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(), + ), + ( + InputMode::Normal, + KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(), + ), + ( + InputMode::Pane, + KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(), + ), + ( + InputMode::Tab, + KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(), + ), + ( + InputMode::Resize, + KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(), + ), + ( + InputMode::Move, + KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(), + ), + ( + InputMode::Search, + KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(), + ), + ( + InputMode::Session, + KeyWithModifier::new(BareKey::Char('g')).with_ctrl_modifier(), + ), + ]; + let mut keys_to_rebind = vec![ + ( + InputMode::Locked, + KeyWithModifier::new(BareKey::Char('a')).with_ctrl_modifier(), + vec![actions::Action::SwitchToMode(InputMode::Normal)], + ), + ( + InputMode::Normal, + KeyWithModifier::new(BareKey::Char('a')).with_ctrl_modifier(), + vec![actions::Action::SwitchToMode(InputMode::Locked)], + ), + ( + InputMode::Pane, + KeyWithModifier::new(BareKey::Char('a')).with_ctrl_modifier(), + vec![actions::Action::SwitchToMode(InputMode::Locked)], + ), + ( + InputMode::Tab, + KeyWithModifier::new(BareKey::Char('a')).with_ctrl_modifier(), + vec![actions::Action::SwitchToMode(InputMode::Locked)], + ), + ( + InputMode::Resize, + KeyWithModifier::new(BareKey::Char('a')).with_ctrl_modifier(), + vec![actions::Action::SwitchToMode(InputMode::Locked)], + ), + ( + InputMode::Move, + KeyWithModifier::new(BareKey::Char('a')).with_ctrl_modifier(), + vec![actions::Action::SwitchToMode(InputMode::Locked)], + ), + ( + InputMode::Search, + KeyWithModifier::new(BareKey::Char('a')).with_ctrl_modifier(), + vec![actions::Action::SwitchToMode(InputMode::Locked)], + ), + ( + InputMode::Session, + KeyWithModifier::new(BareKey::Char('a')).with_ctrl_modifier(), + vec![actions::Action::SwitchToMode(InputMode::Locked)], + ), + ]; + rebind_keys(keys_to_unbind, keys_to_rebind, write_to_disk); + }, _ => {}, }, Event::CustomMessage(message, payload) => { diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 56daf743f1..95e547b61f 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -42,10 +42,11 @@ use zellij_utils::{ channels::{self, ChannelWithContext, SenderWithContext}, cli::CliArgs, consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE}, - data::{ConnectToSession, Event, InputMode, PluginCapabilities}, + data::{ConnectToSession, Event, InputMode, KeyWithModifier, PluginCapabilities}, errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext}, home::{default_layout_dir, get_default_data_dir}, input::{ + actions::Action, command::{RunCommand, TerminalAction}, config::Config, get_mode_info, @@ -109,6 +110,12 @@ pub enum ServerInstruction { }, ConfigWrittenToDisk(ClientId, Config), FailedToWriteConfigToDisk(ClientId, Option), // Pathbuf - file we failed to write + RebindKeys { + client_id: ClientId, + keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec)>, + keys_to_unbind: Vec<(InputMode, KeyWithModifier)>, + write_config_to_disk: bool, + }, } impl From<&ServerInstruction> for ServerContext { @@ -145,6 +152,7 @@ impl From<&ServerInstruction> for ServerContext { ServerInstruction::FailedToWriteConfigToDisk(..) => { ServerContext::FailedToWriteConfigToDisk }, + ServerInstruction::RebindKeys { .. } => ServerContext::RebindKeys, } } } @@ -226,6 +234,57 @@ impl SessionConfiguration { } (full_reconfigured_config, config_changed) } + pub fn rebind_keys( + &mut self, + client_id: &ClientId, + keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec)>, + keys_to_unbind: Vec<(InputMode, KeyWithModifier)>, + ) -> (Option, bool) { + let mut full_reconfigured_config = None; + let mut config_changed = false; + + if self.runtime_config.get(client_id).is_none() { + if let Some(saved_config) = self.saved_config.get(client_id) { + self.runtime_config.insert(*client_id, saved_config.clone()); + } + } + match self.runtime_config.get_mut(client_id) { + Some(config) => { + for (input_mode, key_with_modifier) in keys_to_unbind { + let keys_in_mode = config + .keybinds + .0 + .entry(input_mode) + .or_insert_with(Default::default); + let removed = keys_in_mode.remove(&key_with_modifier); + if removed.is_some() { + config_changed = true; + } + } + for (input_mode, key_with_modifier, actions) in keys_to_rebind { + let keys_in_mode = config + .keybinds + .0 + .entry(input_mode) + .or_insert_with(Default::default); + if keys_in_mode.get(&key_with_modifier) != Some(&actions) { + config_changed = true; + keys_in_mode.insert(key_with_modifier, actions); + } + } + if config_changed { + full_reconfigured_config = Some(config.clone()); + } + }, + None => { + log::error!( + "Could not find runtime or saved configuration for client, cannot rebind keys" + ); + }, + } + + (full_reconfigured_config, config_changed) + } } pub(crate) struct SessionMetaData { @@ -1120,6 +1179,42 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .send_to_plugin(PluginInstruction::FailedToWriteConfigToDisk { file_path }) .unwrap(); }, + ServerInstruction::RebindKeys { + client_id, + keys_to_rebind, + keys_to_unbind, + write_config_to_disk, + } => { + let (new_config, runtime_config_changed) = session_data + .write() + .unwrap() + .as_mut() + .unwrap() + .session_configuration + .rebind_keys(&client_id, keys_to_rebind, keys_to_unbind); + if let Some(new_config) = new_config { + if write_config_to_disk { + let clear_defaults = true; + send_to_client!( + client_id, + os_input, + ServerToClientMsg::WriteConfigToDisk { + config: new_config.to_string(clear_defaults) + }, + session_state + ); + } + + if runtime_config_changed { + session_data + .write() + .unwrap() + .as_mut() + .unwrap() + .propagate_configuration_changes(vec![(client_id, new_config)]); + } + } + }, } } diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 630ae70b81..0750c0ddde 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -8470,3 +8470,84 @@ pub fn load_new_plugin_plugin_command() { .count(); assert_eq!(request_state_update_requests, 3); } + +#[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_struct!( + 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, + Some(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('y')).with_alt_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_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_event)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rebind_keys_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rebind_keys_plugin_command.snap index b5375e9695..91457bb2a2 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rebind_keys_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__rebind_keys_plugin_command.snap @@ -1,11 +1,231 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 6566 -expression: "format!(\"{:#?}\", rebind_keys_event)" +assertion_line: 8552 +expression: "format!(\"{:#?}\", rebind_event)" --- Some( - RebindKeys( - 1, - "\n keybinds {\n locked {\n bind \"a\" { NewTab; }\n }\n }\n ", - ), + RebindKeys { + client_id: 1, + keys_to_rebind: [ + ( + Locked, + KeyWithModifier { + bare_key: Char( + 'a', + ), + key_modifiers: { + Ctrl, + }, + }, + [ + SwitchToMode( + Normal, + ), + ], + ), + ( + Normal, + KeyWithModifier { + bare_key: Char( + 'a', + ), + key_modifiers: { + Ctrl, + }, + }, + [ + SwitchToMode( + Locked, + ), + ], + ), + ( + Pane, + KeyWithModifier { + bare_key: Char( + 'a', + ), + key_modifiers: { + Ctrl, + }, + }, + [ + SwitchToMode( + Locked, + ), + ], + ), + ( + Tab, + KeyWithModifier { + bare_key: Char( + 'a', + ), + key_modifiers: { + Ctrl, + }, + }, + [ + SwitchToMode( + Locked, + ), + ], + ), + ( + Resize, + KeyWithModifier { + bare_key: Char( + 'a', + ), + key_modifiers: { + Ctrl, + }, + }, + [ + SwitchToMode( + Locked, + ), + ], + ), + ( + Move, + KeyWithModifier { + bare_key: Char( + 'a', + ), + key_modifiers: { + Ctrl, + }, + }, + [ + SwitchToMode( + Locked, + ), + ], + ), + ( + Search, + KeyWithModifier { + bare_key: Char( + 'a', + ), + key_modifiers: { + Ctrl, + }, + }, + [ + SwitchToMode( + Locked, + ), + ], + ), + ( + Session, + KeyWithModifier { + bare_key: Char( + 'a', + ), + key_modifiers: { + Ctrl, + }, + }, + [ + SwitchToMode( + Locked, + ), + ], + ), + ], + keys_to_unbind: [ + ( + Locked, + KeyWithModifier { + bare_key: Char( + 'g', + ), + key_modifiers: { + Ctrl, + }, + }, + ), + ( + Normal, + KeyWithModifier { + bare_key: Char( + 'g', + ), + key_modifiers: { + Ctrl, + }, + }, + ), + ( + Pane, + KeyWithModifier { + bare_key: Char( + 'g', + ), + key_modifiers: { + Ctrl, + }, + }, + ), + ( + Tab, + KeyWithModifier { + bare_key: Char( + 'g', + ), + key_modifiers: { + Ctrl, + }, + }, + ), + ( + Resize, + KeyWithModifier { + bare_key: Char( + 'g', + ), + key_modifiers: { + Ctrl, + }, + }, + ), + ( + Move, + KeyWithModifier { + bare_key: Char( + 'g', + ), + key_modifiers: { + Ctrl, + }, + }, + ), + ( + Search, + KeyWithModifier { + bare_key: Char( + 'g', + ), + key_modifiers: { + Ctrl, + }, + }, + ), + ( + Session, + KeyWithModifier { + bare_key: Char( + 'g', + ), + key_modifiers: { + Ctrl, + }, + }, + ), + ], + write_config_to_disk: true, + }, ) diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 05ae175252..5dd374859a 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -18,8 +18,8 @@ use std::{ }; use wasmtime::{Caller, Linker}; use zellij_utils::data::{ - CommandType, ConnectToSession, FloatingPaneCoordinates, HttpVerb, LayoutInfo, MessageToPlugin, - OriginatingPlugin, PermissionStatus, PermissionType, PluginPermission, + CommandType, ConnectToSession, FloatingPaneCoordinates, HttpVerb, KeyWithModifier, LayoutInfo, + MessageToPlugin, OriginatingPlugin, PermissionStatus, PermissionType, PluginPermission, }; use zellij_utils::input::permission::PermissionCache; use zellij_utils::{ @@ -346,6 +346,11 @@ fn host_run_plugin_command(caller: Caller<'_, PluginEnv>) { load_in_background, skip_plugin_cache, } => load_new_plugin(env, url, config, load_in_background, skip_plugin_cache), + PluginCommand::RebindKeys { + keys_to_rebind, + keys_to_unbind, + write_config_to_disk, + } => rebind_keys(env, keys_to_rebind, keys_to_unbind, write_config_to_disk)?, }, (PermissionStatus::Denied, permission) => { log::error!( @@ -970,6 +975,25 @@ fn reconfigure(env: &PluginEnv, new_config: String, write_config_to_disk: bool) Ok(()) } +fn rebind_keys( + env: &PluginEnv, + keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec)>, + keys_to_unbind: Vec<(InputMode, KeyWithModifier)>, + write_config_to_disk: bool, +) -> Result<()> { + let err_context = || "Failed to rebind_keys"; + let client_id = env.client_id; + env.senders + .send_to_server(ServerInstruction::RebindKeys { + client_id, + keys_to_rebind, + keys_to_unbind, + write_config_to_disk, + }) + .with_context(err_context)?; + Ok(()) +} + fn switch_to_mode(env: &PluginEnv, input_mode: InputMode) { let action = Action::SwitchToMode(input_mode); let error_msg = || format!("failed to switch to mode in plugin {}", env.name()); @@ -1874,7 +1898,9 @@ fn check_command_permission( | PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes, PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins, PluginCommand::DumpSessionLayout => PermissionType::ReadApplicationState, - PluginCommand::Reconfigure(..) => PermissionType::Reconfigure, + PluginCommand::RebindKeys { .. } | PluginCommand::Reconfigure(..) => { + PermissionType::Reconfigure + }, _ => return (PermissionStatus::Granted, None), }; diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 7e81d5050f..c51a93d88e 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -6,6 +6,7 @@ use std::{ }; use zellij_utils::data::*; use zellij_utils::errors::prelude::*; +use zellij_utils::input::actions::Action; pub use zellij_utils::plugin_api; use zellij_utils::plugin_api::plugin_command::ProtobufPluginCommand; use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion}; @@ -853,7 +854,7 @@ pub fn dump_session_layout() { unsafe { host_run_plugin_command() }; } -/// Rebind keys for the current user +/// Change configuration for the current user pub fn reconfigure(new_config: String, save_configuration_file: bool) { let plugin_command = PluginCommand::Reconfigure(new_config, save_configuration_file); let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); @@ -1102,6 +1103,22 @@ pub fn load_new_plugin>( unsafe { host_run_plugin_command() }; } +/// Rebind keys for the current user +pub fn rebind_keys( + keys_to_unbind: Vec<(InputMode, KeyWithModifier)>, + keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec)>, + write_config_to_disk: bool, +) { + let plugin_command = PluginCommand::RebindKeys { + keys_to_rebind, + keys_to_unbind, + write_config_to_disk, + }; + 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)] diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index 19c146acc5..f3bd625bea 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -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, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87" + 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, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88" )] pub payload: ::core::option::Option, } @@ -172,10 +172,40 @@ pub mod plugin_command { ReloadPluginPayload(super::ReloadPluginPayload), #[prost(message, tag = "87")] LoadNewPluginPayload(super::LoadNewPluginPayload), + #[prost(message, tag = "88")] + RebindKeysPayload(super::RebindKeysPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct RebindKeysPayload { + #[prost(message, repeated, tag = "1")] + pub keys_to_rebind: ::prost::alloc::vec::Vec, + #[prost(message, repeated, tag = "2")] + pub keys_to_unbind: ::prost::alloc::vec::Vec, + #[prost(bool, tag = "3")] + pub write_config_to_disk: bool, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KeyToRebind { + #[prost(enumeration = "super::input_mode::InputMode", tag = "1")] + pub input_mode: i32, + #[prost(message, optional, tag = "2")] + pub key: ::core::option::Option, + #[prost(message, repeated, tag = "3")] + pub actions: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct KeyToUnbind { + #[prost(enumeration = "super::input_mode::InputMode", tag = "1")] + pub input_mode: i32, + #[prost(message, optional, tag = "2")] + pub key: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct LoadNewPluginPayload { #[prost(string, tag = "1")] pub plugin_url: ::prost::alloc::string::String, @@ -684,6 +714,7 @@ pub enum CommandName { BreakPanesToTabWithIndex = 109, ReloadPlugin = 110, LoadNewPlugin = 111, + RebindKeys = 112, } impl CommandName { /// String value of the enum field names used in the ProtoBuf definition. @@ -806,6 +837,7 @@ impl CommandName { CommandName::BreakPanesToTabWithIndex => "BreakPanesToTabWithIndex", CommandName::ReloadPlugin => "ReloadPlugin", CommandName::LoadNewPlugin => "LoadNewPlugin", + CommandName::RebindKeys => "RebindKeys", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -925,6 +957,7 @@ impl CommandName { "BreakPanesToTabWithIndex" => Some(Self::BreakPanesToTabWithIndex), "ReloadPlugin" => Some(Self::ReloadPlugin), "LoadNewPlugin" => Some(Self::LoadNewPlugin), + "RebindKeys" => Some(Self::RebindKeys), _ => None, } } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 64cded6b5f..6220aa46dc 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -1873,4 +1873,9 @@ pub enum PluginCommand { load_in_background: bool, skip_plugin_cache: bool, }, + RebindKeys { + keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec)>, + keys_to_unbind: Vec<(InputMode, KeyWithModifier)>, + write_config_to_disk: bool, + }, } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 4de67dab6e..151e6e183d 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -482,6 +482,7 @@ pub enum ServerContext { Reconfigure, ConfigWrittenToDisk, FailedToWriteConfigToDisk, + RebindKeys, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index adb91e5392..29ebca670b 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -7,6 +7,8 @@ import "command.proto"; import "message.proto"; import "resize.proto"; import "plugin_permission.proto"; +import "input_mode.proto"; +import "key.proto"; package api.plugin_command; @@ -123,6 +125,7 @@ enum CommandName { BreakPanesToTabWithIndex = 109; ReloadPlugin = 110; LoadNewPlugin = 111; + RebindKeys = 112; } message PluginCommand { @@ -205,9 +208,27 @@ message PluginCommand { BreakPanesToTabWithIndexPayload break_panes_to_tab_with_index_payload = 85; ReloadPluginPayload reload_plugin_payload = 86; LoadNewPluginPayload load_new_plugin_payload = 87; + RebindKeysPayload rebind_keys_payload = 88; } } +message RebindKeysPayload { + repeated KeyToRebind keys_to_rebind = 1; + repeated KeyToUnbind keys_to_unbind = 2; + bool write_config_to_disk = 3; +} + +message KeyToRebind { + input_mode.InputMode input_mode = 1; + key.Key key = 2; + repeated action.Action actions = 3; +} + +message KeyToUnbind { + input_mode.InputMode input_mode = 1; + key.Key key = 2; +} + message LoadNewPluginPayload { string plugin_url = 1; repeated ContextItem plugin_config = 2; diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index d455500a92..79b087944a 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -9,28 +9,29 @@ pub use super::generated_api::api::{ FixedOrPercent as ProtobufFixedOrPercent, FixedOrPercentValue as ProtobufFixedOrPercentValue, FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, HidePaneWithIdPayload, - HttpVerb as ProtobufHttpVerb, IdAndNewName, KillSessionsPayload, LoadNewPluginPayload, - MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload, - MovePayload, NewPluginArgs as ProtobufNewPluginArgs, NewTabsWithLayoutInfoPayload, - OpenCommandPanePayload, OpenFilePayload, PageScrollDownInPaneIdPayload, - PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId, PaneType as ProtobufPaneType, - PluginCommand as ProtobufPluginCommand, PluginMessagePayload, ReconfigurePayload, - ReloadPluginPayload, RequestPluginPermissionPayload, RerunCommandPanePayload, - ResizePaneIdWithDirectionPayload, ResizePayload, RunCommandPayload, - ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, - ScrollUpInPaneIdPayload, SetTimeoutPayload, ShowPaneWithIdPayload, SubscribePayload, - SwitchSessionPayload, SwitchTabToPayload, TogglePaneEmbedOrEjectForPaneIdPayload, - TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload, - WriteCharsToPaneIdPayload, WriteToPaneIdPayload, + HttpVerb as ProtobufHttpVerb, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload, + LoadNewPluginPayload, MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, + MovePaneWithPaneIdPayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs, + NewTabsWithLayoutInfoPayload, OpenCommandPanePayload, OpenFilePayload, + PageScrollDownInPaneIdPayload, PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId, + PaneType as ProtobufPaneType, PluginCommand as ProtobufPluginCommand, PluginMessagePayload, + RebindKeysPayload, ReconfigurePayload, ReloadPluginPayload, RequestPluginPermissionPayload, + RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload, + RunCommandPayload, ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, + ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SetTimeoutPayload, + ShowPaneWithIdPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, + TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, UnsubscribePayload, + WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload, }, plugin_permission::PermissionType as ProtobufPermissionType, resize::ResizeAction as ProtobufResizeAction, }; use crate::data::{ - ConnectToSession, FloatingPaneCoordinates, HttpVerb, MessageToPlugin, NewPluginArgs, PaneId, - PermissionType, PluginCommand, + ConnectToSession, FloatingPaneCoordinates, HttpVerb, InputMode, KeyWithModifier, + MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand, }; +use crate::input::actions::Action; use crate::input::layout::SplitSize; use std::collections::BTreeMap; @@ -184,6 +185,60 @@ impl TryFrom for ProtobufPaneId { } } +impl TryFrom<(InputMode, KeyWithModifier, Vec)> for KeyToRebind { + type Error = &'static str; + fn try_from( + key_to_rebind: (InputMode, KeyWithModifier, Vec), + ) -> Result { + Ok(KeyToRebind { + input_mode: key_to_rebind.0 as i32, + key: Some(key_to_rebind.1.try_into()?), + actions: key_to_rebind + .2 + .into_iter() + .filter_map(|a| a.try_into().ok()) + .collect(), + }) + } +} + +impl TryFrom<(InputMode, KeyWithModifier)> for KeyToUnbind { + type Error = &'static str; + fn try_from(key_to_unbind: (InputMode, KeyWithModifier)) -> Result { + Ok(KeyToUnbind { + input_mode: key_to_unbind.0 as i32, + key: Some(key_to_unbind.1.try_into()?), + }) + } +} + +fn key_to_rebind_to_plugin_command_assets( + key_to_rebind: KeyToRebind, +) -> Option<(InputMode, KeyWithModifier, Vec)> { + Some(( + ProtobufInputMode::from_i32(key_to_rebind.input_mode)? + .try_into() + .ok()?, + key_to_rebind.key?.try_into().ok()?, + key_to_rebind + .actions + .into_iter() + .filter_map(|a| a.try_into().ok()) + .collect(), + )) +} + +fn key_to_unbind_to_plugin_command_assets( + key_to_unbind: KeyToUnbind, +) -> Option<(InputMode, KeyWithModifier)> { + Some(( + ProtobufInputMode::from_i32(key_to_unbind.input_mode)? + .try_into() + .ok()?, + key_to_unbind.key?.try_into().ok()?, + )) +} + impl TryFrom for PluginCommand { type Error = &'static str; fn try_from(protobuf_plugin_command: ProtobufPluginCommand) -> Result { @@ -1226,6 +1281,24 @@ impl TryFrom for PluginCommand { }, _ => Err("Mismatched payload for LoadNewPlugin"), }, + Some(CommandName::RebindKeys) => match protobuf_plugin_command.payload { + Some(Payload::RebindKeysPayload(rebind_keys_payload)) => { + Ok(PluginCommand::RebindKeys { + keys_to_rebind: rebind_keys_payload + .keys_to_rebind + .into_iter() + .filter_map(|k| key_to_rebind_to_plugin_command_assets(k)) + .collect(), + keys_to_unbind: rebind_keys_payload + .keys_to_unbind + .into_iter() + .filter_map(|k| key_to_unbind_to_plugin_command_assets(k)) + .collect(), + write_config_to_disk: rebind_keys_payload.write_config_to_disk, + }) + }, + _ => Err("Mismatched payload for RebindKeys"), + }, None => Err("Unrecognized plugin command"), } } @@ -2031,6 +2104,24 @@ impl TryFrom for ProtobufPluginCommand { should_load_plugin_in_background: load_in_background, })), }), + PluginCommand::RebindKeys { + keys_to_rebind, + keys_to_unbind, + write_config_to_disk, + } => Ok(ProtobufPluginCommand { + name: CommandName::RebindKeys as i32, + payload: Some(Payload::RebindKeysPayload(RebindKeysPayload { + keys_to_rebind: keys_to_rebind + .into_iter() + .filter_map(|k| k.try_into().ok()) + .collect(), + keys_to_unbind: keys_to_unbind + .into_iter() + .filter_map(|k| k.try_into().ok()) + .collect(), + write_config_to_disk, + })), + }), } } }