feat(plugins): rebind keys api (#3680)

* feat(plugins): add API to explicitly unbind/rebind specific keys in specific modes

* style(fmt): rustfmt
This commit is contained in:
Aram Drevekenin 2024-10-15 17:27:44 +02:00 committed by GitHub
parent 5ae36ed58f
commit 0c9d72c51e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 697 additions and 27 deletions

View file

@ -445,6 +445,86 @@ impl ZellijPlugin for State {
skip_plugin_cache, 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) => { Event::CustomMessage(message, payload) => {

View file

@ -42,10 +42,11 @@ 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, InputMode, PluginCapabilities}, data::{ConnectToSession, Event, InputMode, KeyWithModifier, 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::{
actions::Action,
command::{RunCommand, TerminalAction}, command::{RunCommand, TerminalAction},
config::Config, config::Config,
get_mode_info, get_mode_info,
@ -109,6 +110,12 @@ pub enum ServerInstruction {
}, },
ConfigWrittenToDisk(ClientId, Config), ConfigWrittenToDisk(ClientId, Config),
FailedToWriteConfigToDisk(ClientId, Option<PathBuf>), // Pathbuf - file we failed to write FailedToWriteConfigToDisk(ClientId, Option<PathBuf>), // Pathbuf - file we failed to write
RebindKeys {
client_id: ClientId,
keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec<Action>)>,
keys_to_unbind: Vec<(InputMode, KeyWithModifier)>,
write_config_to_disk: bool,
},
} }
impl From<&ServerInstruction> for ServerContext { impl From<&ServerInstruction> for ServerContext {
@ -145,6 +152,7 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::FailedToWriteConfigToDisk(..) => { ServerInstruction::FailedToWriteConfigToDisk(..) => {
ServerContext::FailedToWriteConfigToDisk ServerContext::FailedToWriteConfigToDisk
}, },
ServerInstruction::RebindKeys { .. } => ServerContext::RebindKeys,
} }
} }
} }
@ -226,6 +234,57 @@ impl SessionConfiguration {
} }
(full_reconfigured_config, config_changed) (full_reconfigured_config, config_changed)
} }
pub fn rebind_keys(
&mut self,
client_id: &ClientId,
keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec<Action>)>,
keys_to_unbind: Vec<(InputMode, KeyWithModifier)>,
) -> (Option<Config>, 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 { pub(crate) struct SessionMetaData {
@ -1120,6 +1179,42 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.send_to_plugin(PluginInstruction::FailedToWriteConfigToDisk { file_path }) .send_to_plugin(PluginInstruction::FailedToWriteConfigToDisk { file_path })
.unwrap(); .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)]);
}
}
},
} }
} }

View file

@ -8470,3 +8470,84 @@ pub fn load_new_plugin_plugin_command() {
.count(); .count();
assert_eq!(request_state_update_requests, 3); 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));
}

View file

@ -1,11 +1,231 @@
--- ---
source: zellij-server/src/plugins/./unit/plugin_tests.rs source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 6566 assertion_line: 8552
expression: "format!(\"{:#?}\", rebind_keys_event)" expression: "format!(\"{:#?}\", rebind_event)"
--- ---
Some( Some(
RebindKeys( RebindKeys {
1, client_id: 1,
"\n keybinds {\n locked {\n bind \"a\" { NewTab; }\n }\n }\n ", 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,
},
) )

View file

@ -18,8 +18,8 @@ use std::{
}; };
use wasmtime::{Caller, Linker}; use wasmtime::{Caller, Linker};
use zellij_utils::data::{ use zellij_utils::data::{
CommandType, ConnectToSession, FloatingPaneCoordinates, HttpVerb, LayoutInfo, MessageToPlugin, CommandType, ConnectToSession, FloatingPaneCoordinates, HttpVerb, KeyWithModifier, LayoutInfo,
OriginatingPlugin, PermissionStatus, PermissionType, PluginPermission, MessageToPlugin, OriginatingPlugin, PermissionStatus, PermissionType, PluginPermission,
}; };
use zellij_utils::input::permission::PermissionCache; use zellij_utils::input::permission::PermissionCache;
use zellij_utils::{ use zellij_utils::{
@ -346,6 +346,11 @@ fn host_run_plugin_command(caller: Caller<'_, PluginEnv>) {
load_in_background, load_in_background,
skip_plugin_cache, skip_plugin_cache,
} => load_new_plugin(env, url, config, 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) => { (PermissionStatus::Denied, permission) => {
log::error!( log::error!(
@ -970,6 +975,25 @@ fn reconfigure(env: &PluginEnv, new_config: String, write_config_to_disk: bool)
Ok(()) Ok(())
} }
fn rebind_keys(
env: &PluginEnv,
keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec<Action>)>,
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) { fn switch_to_mode(env: &PluginEnv, input_mode: InputMode) {
let action = Action::SwitchToMode(input_mode); let action = Action::SwitchToMode(input_mode);
let error_msg = || format!("failed to switch to mode in plugin {}", env.name()); 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::CliPipeOutput(..) => PermissionType::ReadCliPipes,
PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins, PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins,
PluginCommand::DumpSessionLayout => PermissionType::ReadApplicationState, PluginCommand::DumpSessionLayout => PermissionType::ReadApplicationState,
PluginCommand::Reconfigure(..) => PermissionType::Reconfigure, PluginCommand::RebindKeys { .. } | PluginCommand::Reconfigure(..) => {
PermissionType::Reconfigure
},
_ => return (PermissionStatus::Granted, None), _ => return (PermissionStatus::Granted, None),
}; };

View file

@ -6,6 +6,7 @@ use std::{
}; };
use zellij_utils::data::*; use zellij_utils::data::*;
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
use zellij_utils::input::actions::Action;
pub use zellij_utils::plugin_api; pub use zellij_utils::plugin_api;
use zellij_utils::plugin_api::plugin_command::ProtobufPluginCommand; use zellij_utils::plugin_api::plugin_command::ProtobufPluginCommand;
use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion}; use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion};
@ -853,7 +854,7 @@ pub fn dump_session_layout() {
unsafe { host_run_plugin_command() }; 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) { pub fn reconfigure(new_config: String, save_configuration_file: bool) {
let plugin_command = PluginCommand::Reconfigure(new_config, save_configuration_file); let plugin_command = PluginCommand::Reconfigure(new_config, save_configuration_file);
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
@ -1102,6 +1103,22 @@ pub fn load_new_plugin<S: AsRef<str>>(
unsafe { host_run_plugin_command() }; 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<Action>)>,
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 // 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, 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<plugin_command::Payload>, pub payload: ::core::option::Option<plugin_command::Payload>,
} }
@ -172,10 +172,40 @@ pub mod plugin_command {
ReloadPluginPayload(super::ReloadPluginPayload), ReloadPluginPayload(super::ReloadPluginPayload),
#[prost(message, tag = "87")] #[prost(message, tag = "87")]
LoadNewPluginPayload(super::LoadNewPluginPayload), LoadNewPluginPayload(super::LoadNewPluginPayload),
#[prost(message, tag = "88")]
RebindKeysPayload(super::RebindKeysPayload),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct RebindKeysPayload {
#[prost(message, repeated, tag = "1")]
pub keys_to_rebind: ::prost::alloc::vec::Vec<KeyToRebind>,
#[prost(message, repeated, tag = "2")]
pub keys_to_unbind: ::prost::alloc::vec::Vec<KeyToUnbind>,
#[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<super::key::Key>,
#[prost(message, repeated, tag = "3")]
pub actions: ::prost::alloc::vec::Vec<super::action::Action>,
}
#[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<super::key::Key>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct LoadNewPluginPayload { pub struct LoadNewPluginPayload {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub plugin_url: ::prost::alloc::string::String, pub plugin_url: ::prost::alloc::string::String,
@ -684,6 +714,7 @@ pub enum CommandName {
BreakPanesToTabWithIndex = 109, BreakPanesToTabWithIndex = 109,
ReloadPlugin = 110, ReloadPlugin = 110,
LoadNewPlugin = 111, LoadNewPlugin = 111,
RebindKeys = 112,
} }
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.
@ -806,6 +837,7 @@ impl CommandName {
CommandName::BreakPanesToTabWithIndex => "BreakPanesToTabWithIndex", CommandName::BreakPanesToTabWithIndex => "BreakPanesToTabWithIndex",
CommandName::ReloadPlugin => "ReloadPlugin", CommandName::ReloadPlugin => "ReloadPlugin",
CommandName::LoadNewPlugin => "LoadNewPlugin", CommandName::LoadNewPlugin => "LoadNewPlugin",
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.
@ -925,6 +957,7 @@ impl CommandName {
"BreakPanesToTabWithIndex" => Some(Self::BreakPanesToTabWithIndex), "BreakPanesToTabWithIndex" => Some(Self::BreakPanesToTabWithIndex),
"ReloadPlugin" => Some(Self::ReloadPlugin), "ReloadPlugin" => Some(Self::ReloadPlugin),
"LoadNewPlugin" => Some(Self::LoadNewPlugin), "LoadNewPlugin" => Some(Self::LoadNewPlugin),
"RebindKeys" => Some(Self::RebindKeys),
_ => None, _ => None,
} }
} }

View file

@ -1873,4 +1873,9 @@ pub enum PluginCommand {
load_in_background: bool, load_in_background: bool,
skip_plugin_cache: bool, skip_plugin_cache: bool,
}, },
RebindKeys {
keys_to_rebind: Vec<(InputMode, KeyWithModifier, Vec<Action>)>,
keys_to_unbind: Vec<(InputMode, KeyWithModifier)>,
write_config_to_disk: bool,
},
} }

View file

@ -484,6 +484,7 @@ pub enum ServerContext {
Reconfigure, Reconfigure,
ConfigWrittenToDisk, ConfigWrittenToDisk,
FailedToWriteConfigToDisk, FailedToWriteConfigToDisk,
RebindKeys,
} }
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]

View file

@ -7,6 +7,8 @@ import "command.proto";
import "message.proto"; import "message.proto";
import "resize.proto"; import "resize.proto";
import "plugin_permission.proto"; import "plugin_permission.proto";
import "input_mode.proto";
import "key.proto";
package api.plugin_command; package api.plugin_command;
@ -123,6 +125,7 @@ enum CommandName {
BreakPanesToTabWithIndex = 109; BreakPanesToTabWithIndex = 109;
ReloadPlugin = 110; ReloadPlugin = 110;
LoadNewPlugin = 111; LoadNewPlugin = 111;
RebindKeys = 112;
} }
message PluginCommand { message PluginCommand {
@ -205,9 +208,27 @@ message PluginCommand {
BreakPanesToTabWithIndexPayload break_panes_to_tab_with_index_payload = 85; BreakPanesToTabWithIndexPayload break_panes_to_tab_with_index_payload = 85;
ReloadPluginPayload reload_plugin_payload = 86; ReloadPluginPayload reload_plugin_payload = 86;
LoadNewPluginPayload load_new_plugin_payload = 87; 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 { message LoadNewPluginPayload {
string plugin_url = 1; string plugin_url = 1;
repeated ContextItem plugin_config = 2; repeated ContextItem plugin_config = 2;

View file

@ -9,28 +9,29 @@ pub use super::generated_api::api::{
FixedOrPercent as ProtobufFixedOrPercent, FixedOrPercent as ProtobufFixedOrPercent,
FixedOrPercentValue as ProtobufFixedOrPercentValue, FixedOrPercentValue as ProtobufFixedOrPercentValue,
FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, HidePaneWithIdPayload, FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, HidePaneWithIdPayload,
HttpVerb as ProtobufHttpVerb, IdAndNewName, KillSessionsPayload, LoadNewPluginPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload,
MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload, LoadNewPluginPayload, MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload,
MovePayload, NewPluginArgs as ProtobufNewPluginArgs, NewTabsWithLayoutInfoPayload, MovePaneWithPaneIdPayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs,
OpenCommandPanePayload, OpenFilePayload, PageScrollDownInPaneIdPayload, NewTabsWithLayoutInfoPayload, OpenCommandPanePayload, OpenFilePayload,
PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId, PaneType as ProtobufPaneType, PageScrollDownInPaneIdPayload, PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId,
PluginCommand as ProtobufPluginCommand, PluginMessagePayload, ReconfigurePayload, PaneType as ProtobufPaneType, PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
ReloadPluginPayload, RequestPluginPermissionPayload, RerunCommandPanePayload, RebindKeysPayload, ReconfigurePayload, ReloadPluginPayload, RequestPluginPermissionPayload,
ResizePaneIdWithDirectionPayload, ResizePayload, RunCommandPayload, RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload,
ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, RunCommandPayload, ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload,
ScrollUpInPaneIdPayload, SetTimeoutPayload, ShowPaneWithIdPayload, SubscribePayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SetTimeoutPayload,
SwitchSessionPayload, SwitchTabToPayload, TogglePaneEmbedOrEjectForPaneIdPayload, ShowPaneWithIdPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload,
TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload, TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, UnsubscribePayload,
WriteCharsToPaneIdPayload, WriteToPaneIdPayload, WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload,
}, },
plugin_permission::PermissionType as ProtobufPermissionType, plugin_permission::PermissionType as ProtobufPermissionType,
resize::ResizeAction as ProtobufResizeAction, resize::ResizeAction as ProtobufResizeAction,
}; };
use crate::data::{ use crate::data::{
ConnectToSession, FloatingPaneCoordinates, HttpVerb, MessageToPlugin, NewPluginArgs, PaneId, ConnectToSession, FloatingPaneCoordinates, HttpVerb, InputMode, KeyWithModifier,
PermissionType, PluginCommand, MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand,
}; };
use crate::input::actions::Action;
use crate::input::layout::SplitSize; use crate::input::layout::SplitSize;
use std::collections::BTreeMap; use std::collections::BTreeMap;
@ -184,6 +185,60 @@ impl TryFrom<PaneId> for ProtobufPaneId {
} }
} }
impl TryFrom<(InputMode, KeyWithModifier, Vec<Action>)> for KeyToRebind {
type Error = &'static str;
fn try_from(
key_to_rebind: (InputMode, KeyWithModifier, Vec<Action>),
) -> Result<Self, &'static str> {
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<Self, &'static str> {
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<Action>)> {
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<ProtobufPluginCommand> for PluginCommand { impl TryFrom<ProtobufPluginCommand> for PluginCommand {
type Error = &'static str; type Error = &'static str;
fn try_from(protobuf_plugin_command: ProtobufPluginCommand) -> Result<Self, &'static str> { fn try_from(protobuf_plugin_command: ProtobufPluginCommand) -> Result<Self, &'static str> {
@ -1226,6 +1281,24 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
}, },
_ => Err("Mismatched payload for LoadNewPlugin"), _ => 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"), None => Err("Unrecognized plugin command"),
} }
} }
@ -2031,6 +2104,24 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
should_load_plugin_in_background: load_in_background, 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,
})),
}),
} }
} }
} }