Skip to content

Commit

Permalink
feat(plugins): rebind keys api (zellij-org#3680)
Browse files Browse the repository at this point in the history
* feat(plugins): add API to explicitly unbind/rebind specific keys in specific modes

* style(fmt): rustfmt
  • Loading branch information
imsnif authored and Tomcat-42 committed Nov 9, 2024
1 parent c47c9e0 commit 4549e30
Show file tree
Hide file tree
Showing 11 changed files with 697 additions and 27 deletions.
80 changes: 80 additions & 0 deletions default-plugins/fixture-plugin-for-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
97 changes: 96 additions & 1 deletion zellij-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -109,6 +110,12 @@ pub enum ServerInstruction {
},
ConfigWrittenToDisk(ClientId, Config),
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 {
Expand Down Expand Up @@ -145,6 +152,7 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::FailedToWriteConfigToDisk(..) => {
ServerContext::FailedToWriteConfigToDisk
},
ServerInstruction::RebindKeys { .. } => ServerContext::RebindKeys,
}
}
}
Expand Down Expand Up @@ -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<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 {
Expand Down Expand Up @@ -1120,6 +1179,42 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, 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)]);
}
}
},
}
}

Expand Down
81 changes: 81 additions & 0 deletions zellij-server/src/plugins/unit/plugin_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Loading

0 comments on commit 4549e30

Please sign in to comment.