diff options
| author | Aram Drevekenin <aram@poor.dev> | 2024-07-17 16:08:31 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-17 16:08:31 +0200 |
| commit | 84ff29dd024c73e94f3e65d56d907ae903fe3752 (patch) | |
| tree | b3f01370b3b1af71f018eeb27a1437b75e959721 /zellij-server/src/plugins | |
| parent | docs(changelog): scroll region fix (diff) | |
| download | sl0ppij-84ff29dd024c73e94f3e65d56d907ae903fe3752.tar.gz sl0ppij-84ff29dd024c73e94f3e65d56d907ae903fe3752.tar.xz sl0ppij-84ff29dd024c73e94f3e65d56d907ae903fe3752.zip | |
feat(ui): configuration screen + configure non-colliding keys at runtime (#3502)
* rebind => reconfigure
* persist keybinds and mode info to new tabs
* add configuration plugin
* make tests pass
* remove warnings
* style(fmt): rustfmt
Diffstat (limited to 'zellij-server/src/plugins')
9 files changed, 140 insertions, 14 deletions
diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 639a389e..223ecb55 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -31,6 +31,7 @@ use zellij_utils::{ errors::{prelude::*, ContextType, PluginContext}, input::{ command::TerminalAction, + keybinds::Keybinds, layout::{FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, TiledPaneLayout}, plugins::PluginAliases, }, @@ -142,6 +143,11 @@ pub enum PluginInstruction { message: MessageToPlugin, }, UnblockCliPipes(Vec<PluginRenderAsset>), + Reconfigure { + client_id: ClientId, + keybinds: Option<Keybinds>, + default_mode: Option<InputMode>, + }, WatchFilesystem, Exit, } @@ -182,6 +188,7 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::WatchFilesystem => PluginContext::WatchFilesystem, PluginInstruction::KeybindPipe { .. } => PluginContext::KeybindPipe, PluginInstruction::DumpLayoutToPlugin(..) => PluginContext::DumpLayoutToPlugin, + PluginInstruction::Reconfigure { .. } => PluginContext::Reconfigure, } } } @@ -734,6 +741,15 @@ pub(crate) fn plugin_thread_main( .context("failed to unblock input pipe"); } }, + PluginInstruction::Reconfigure { + client_id, + keybinds, + default_mode, + } => { + wasm_bridge + .reconfigure(client_id, keybinds, default_mode) + .non_fatal(); + }, PluginInstruction::WatchFilesystem => { wasm_bridge.start_fs_watcher_if_not_started(); }, diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs index 6ed36c05..51ca3e16 100644 --- a/zellij-server/src/plugins/plugin_loader.rs +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -29,6 +29,7 @@ use zellij_utils::{ data::{InputMode, PluginCapabilities}, errors::prelude::*, input::command::TerminalAction, + input::keybinds::Keybinds, input::layout::Layout, input::plugins::PluginConfig, ipc::ClientAttributes, @@ -69,6 +70,7 @@ pub struct PluginLoader<'a> { default_layout: Box<Layout>, layout_dir: Option<PathBuf>, default_mode: InputMode, + keybinds: Option<Keybinds>, } impl<'a> PluginLoader<'a> { @@ -89,6 +91,7 @@ impl<'a> PluginLoader<'a> { default_layout: Box<Layout>, layout_dir: Option<PathBuf>, default_mode: InputMode, + keybinds: &HashMap<ClientId, Keybinds>, ) -> Result<()> { let err_context = || format!("failed to reload plugin {plugin_id} from memory"); let mut connected_clients: Vec<ClientId> = @@ -97,6 +100,7 @@ impl<'a> PluginLoader<'a> { return Err(anyhow!("No connected clients, cannot reload plugin")); } let first_client_id = connected_clients.remove(0); + let keybinds = keybinds.get(&first_client_id).cloned(); let mut plugin_loader = PluginLoader::new_from_existing_plugin_attributes( &plugin_cache, @@ -115,6 +119,7 @@ impl<'a> PluginLoader<'a> { default_layout, layout_dir, default_mode, + keybinds, )?; plugin_loader .load_module_from_memory() @@ -152,6 +157,7 @@ impl<'a> PluginLoader<'a> { skip_cache: bool, layout_dir: Option<PathBuf>, default_mode: InputMode, + keybinds: Option<Keybinds>, ) -> Result<()> { let err_context = || format!("failed to start plugin {plugin_id} for client {client_id}"); let mut plugin_loader = PluginLoader::new( @@ -173,6 +179,7 @@ impl<'a> PluginLoader<'a> { default_layout, layout_dir, default_mode, + keybinds, )?; if skip_cache { plugin_loader @@ -226,6 +233,7 @@ impl<'a> PluginLoader<'a> { default_layout: Box<Layout>, layout_dir: Option<PathBuf>, default_mode: InputMode, + keybinds: Option<Keybinds>, ) -> Result<()> { let mut new_plugins = HashSet::new(); for plugin_id in plugin_map.lock().unwrap().plugin_ids() { @@ -249,6 +257,7 @@ impl<'a> PluginLoader<'a> { default_layout.clone(), layout_dir.clone(), default_mode, + keybinds.clone(), )?; plugin_loader .load_module_from_memory() @@ -278,6 +287,7 @@ impl<'a> PluginLoader<'a> { default_layout: Box<Layout>, layout_dir: Option<PathBuf>, default_mode: InputMode, + keybinds: &HashMap<ClientId, Keybinds>, ) -> Result<()> { let err_context = || format!("failed to reload plugin id {plugin_id}"); @@ -287,6 +297,7 @@ impl<'a> PluginLoader<'a> { return Err(anyhow!("No connected clients, cannot reload plugin")); } let first_client_id = connected_clients.remove(0); + let keybinds = keybinds.get(&first_client_id).cloned(); let mut plugin_loader = PluginLoader::new_from_existing_plugin_attributes( &plugin_cache, @@ -305,6 +316,7 @@ impl<'a> PluginLoader<'a> { default_layout, layout_dir, default_mode, + keybinds, )?; plugin_loader .compile_module() @@ -338,6 +350,7 @@ impl<'a> PluginLoader<'a> { default_layout: Box<Layout>, layout_dir: Option<PathBuf>, default_mode: InputMode, + keybinds: Option<Keybinds>, ) -> Result<Self> { let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR .join(Url::from(&plugin.location).to_string()) @@ -366,6 +379,7 @@ impl<'a> PluginLoader<'a> { default_layout, layout_dir, default_mode, + keybinds, }) } pub fn new_from_existing_plugin_attributes( @@ -385,6 +399,7 @@ impl<'a> PluginLoader<'a> { default_layout: Box<Layout>, layout_dir: Option<PathBuf>, default_mode: InputMode, + keybinds: Option<Keybinds>, ) -> Result<Self> { let err_context = || "Failed to find existing plugin"; let (running_plugin, _subscriptions, _workers) = { @@ -420,6 +435,7 @@ impl<'a> PluginLoader<'a> { default_layout, layout_dir, default_mode, + keybinds, ) } pub fn new_from_different_client_id( @@ -439,6 +455,7 @@ impl<'a> PluginLoader<'a> { default_layout: Box<Layout>, layout_dir: Option<PathBuf>, default_mode: InputMode, + keybinds: Option<Keybinds>, ) -> Result<Self> { let err_context = || "Failed to find existing plugin"; let running_plugin = { @@ -475,6 +492,7 @@ impl<'a> PluginLoader<'a> { default_layout, layout_dir, default_mode, + keybinds, ) } pub fn load_module_from_memory(&mut self) -> Result<Module> { @@ -731,6 +749,7 @@ impl<'a> PluginLoader<'a> { self.default_layout.clone(), self.layout_dir.clone(), self.default_mode, + self.keybinds.clone(), )?; plugin_loader_for_client .load_module_from_memory() @@ -832,6 +851,11 @@ impl<'a> PluginLoader<'a> { layout_dir: self.layout_dir.clone(), default_mode: self.default_mode.clone(), subscriptions: Arc::new(Mutex::new(HashSet::new())), + keybinds: self + .keybinds + .as_ref() + .unwrap_or_else(|| &self.client_attributes.keybinds) + .clone(), stdin_pipe, stdout_pipe, }; diff --git a/zellij-server/src/plugins/plugin_map.rs b/zellij-server/src/plugins/plugin_map.rs index c5ac6d5f..6390841a 100644 --- a/zellij-server/src/plugins/plugin_map.rs +++ b/zellij-server/src/plugins/plugin_map.rs @@ -22,6 +22,7 @@ use zellij_utils::{ data::InputMode, data::PluginCapabilities, input::command::TerminalAction, + input::keybinds::Keybinds, input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation}, input::plugins::PluginConfig, ipc::ClientAttributes, @@ -289,6 +290,7 @@ pub struct PluginEnv { pub subscriptions: Arc<Mutex<Subscriptions>>, pub stdin_pipe: Arc<Mutex<VecDeque<u8>>>, pub stdout_pipe: Arc<Mutex<VecDeque<u8>>>, + pub keybinds: Keybinds, } #[derive(Clone)] @@ -424,4 +426,10 @@ impl RunningPlugin { false } } + pub fn update_keybinds(&mut self, keybinds: Keybinds) { + self.store.data_mut().keybinds = keybinds; + } + pub fn update_default_mode(&mut self, default_mode: InputMode) { + self.store.data_mut().default_mode = default_mode; + } } diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index c1f4fcd5..5ef220db 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -6492,7 +6492,7 @@ pub fn disconnect_other_clients_plugins_command() { #[test] #[ignore] -pub fn rebind_keys_plugin_command() { +pub fn reconfigure_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()); @@ -6527,7 +6527,7 @@ pub fn rebind_keys_plugin_command() { let received_server_instruction = Arc::new(Mutex::new(vec![])); let server_thread = log_actions_in_thread!( received_server_instruction, - ServerInstruction::RebindKeys, + ServerInstruction::Reconfigure, server_receiver, 1 ); @@ -6555,20 +6555,20 @@ pub fn rebind_keys_plugin_command() { 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 + let reconfigure_event = received_server_instruction .lock() .unwrap() .iter() .rev() .find_map(|i| { - if let ServerInstruction::RebindKeys(..) = i { + if let ServerInstruction::Reconfigure(..) = i { Some(i.clone()) } else { None } }) .clone(); - assert_snapshot!(format!("{:#?}", rebind_keys_event)); + assert_snapshot!(format!("{:#?}", reconfigure_event)); } #[test] diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap index 465f1088..28c7dedc 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 5500 +assertion_line: 5505 expression: "format!(\"{:#?}\", permissions)" --- Some( @@ -14,6 +14,6 @@ Some( WebAccess, ReadCliPipes, MessageAndLaunchOtherPlugins, - RebindKeys, + Reconfigure, ], ) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__reconfigure_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__reconfigure_plugin_command.snap new file mode 100644 index 00000000..3f788d7d --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__reconfigure_plugin_command.snap @@ -0,0 +1,11 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 6571 +expression: "format!(\"{:#?}\", reconfigure_event)" +--- +Some( + Reconfigure( + 1, + "\n keybinds {\n locked {\n bind \"a\" { NewTab; }\n }\n }\n ", + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap index a0fec410..75f47da7 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 5409 +assertion_line: 5414 expression: "format!(\"{:#?}\", new_tab_event)" --- Some( @@ -16,6 +16,6 @@ Some( WebAccess, ReadCliPipes, MessageAndLaunchOtherPlugins, - RebindKeys, + Reconfigure, ], ) diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index 2abc22d3..00a7795b 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -21,6 +21,7 @@ use zellij_utils::async_std::task::{self, JoinHandle}; use zellij_utils::consts::ZELLIJ_CACHE_DIR; use zellij_utils::data::{InputMode, PermissionStatus, PermissionType, PipeMessage, PipeSource}; use zellij_utils::downloader::Downloader; +use zellij_utils::input::keybinds::Keybinds; use zellij_utils::input::permission::PermissionCache; use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap}; use zellij_utils::plugin_api::event::ProtobufEvent; @@ -103,6 +104,7 @@ pub struct WasmBridge { pending_pipes: PendingPipes, layout_dir: Option<PathBuf>, default_mode: InputMode, + keybinds: HashMap<ClientId, Keybinds>, } impl WasmBridge { @@ -149,6 +151,7 @@ impl WasmBridge { pending_pipes: Default::default(), layout_dir, default_mode, + keybinds: HashMap::new(), } } pub fn load_plugin( @@ -206,6 +209,7 @@ impl WasmBridge { let default_layout = self.default_layout.clone(); let layout_dir = self.layout_dir.clone(); let default_mode = self.default_mode; + let keybinds = self.keybinds.get(&client_id).cloned(); async move { let _ = senders.send_to_background_jobs( BackgroundJob::AnimatePluginLoading(plugin_id), @@ -254,6 +258,7 @@ impl WasmBridge { skip_cache, layout_dir, default_mode, + keybinds, ) { Ok(_) => handle_plugin_successful_loading(&senders, plugin_id), Err(e) => handle_plugin_loading_failure( @@ -346,6 +351,7 @@ impl WasmBridge { let default_layout = self.default_layout.clone(); let layout_dir = self.layout_dir.clone(); let default_mode = self.default_mode; + let keybinds = self.keybinds.clone(); async move { match PluginLoader::reload_plugin( first_plugin_id, @@ -364,6 +370,7 @@ impl WasmBridge { default_layout.clone(), layout_dir.clone(), default_mode, + &keybinds, ) { Ok(_) => { handle_plugin_successful_loading(&senders, first_plugin_id); @@ -390,6 +397,7 @@ impl WasmBridge { default_layout.clone(), layout_dir.clone(), default_mode, + &keybinds, ) { Ok(_) => handle_plugin_successful_loading(&senders, *plugin_id), Err(e) => handle_plugin_loading_failure( @@ -443,6 +451,7 @@ impl WasmBridge { self.default_layout.clone(), self.layout_dir.clone(), self.default_mode, + self.keybinds.get(&client_id).cloned(), ) { Ok(_) => { let _ = self @@ -795,6 +804,51 @@ impl WasmBridge { .unwrap() .run_plugin_of_plugin_id(plugin_id) } + + pub fn reconfigure( + &mut self, + client_id: ClientId, + keybinds: Option<Keybinds>, + default_mode: Option<InputMode>, + ) -> Result<()> { + let plugins_to_reconfigure: Vec<Arc<Mutex<RunningPlugin>>> = self + .plugin_map + .lock() + .unwrap() + .running_plugins() + .iter() + .cloned() + .filter_map(|(_plugin_id, c_id, running_plugin)| { + if c_id == client_id { + Some(running_plugin.clone()) + } else { + None + } + }) + .collect(); + if let Some(default_mode) = default_mode.as_ref() { + self.default_mode = *default_mode; + } + if let Some(keybinds) = keybinds.as_ref() { + self.keybinds.insert(client_id, keybinds.clone()); + } + for running_plugin in plugins_to_reconfigure { + task::spawn({ + let running_plugin = running_plugin.clone(); + let keybinds = keybinds.clone(); + async move { + let mut running_plugin = running_plugin.lock().unwrap(); + if let Some(keybinds) = keybinds { + running_plugin.update_keybinds(keybinds); + } + if let Some(default_mode) = default_mode { + running_plugin.update_default_mode(default_mode); + } + } + }); + } + Ok(()) + } fn apply_cached_events_and_resizes_for_plugin( &mut self, plugin_id: PluginId, @@ -1288,6 +1342,18 @@ pub fn apply_event_to_plugin( let err_context = || format!("Failed to apply event to plugin {plugin_id}"); match check_event_permission(running_plugin.store.data(), event) { (PermissionStatus::Granted, _) => { + let mut event = event.clone(); + if let Event::ModeUpdate(mode_info) = &mut event { + // we do this because there can be some cases where this event arrives here with + // the wrong keybindings or default mode (for example: when triggered from the CLI, + // where we do not know the target client_id and thus don't know if their keybindings are the + // default or if they have changed at runtime), the keybindings in running_plugin + // should always be up-to-date. Ideally, we would have changed the keybindings in + // ModeInfo to an Option, but alas - this is already part of our contract and that + // would be a breaking change. + mode_info.keybinds = running_plugin.store.data().keybinds.to_keybinds_vec(); + mode_info.base_mode = Some(running_plugin.store.data().default_mode); + } let protobuf_event: ProtobufEvent = event .clone() .try_into() diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index a517df06..21b8b01d 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -61,6 +61,7 @@ macro_rules! apply_action { $env.default_shell.clone(), $env.default_layout.clone(), None, + $env.keybinds.clone(), $env.default_mode.clone(), ) { log::error!("{}: {:?}", $error_message(), e); @@ -244,7 +245,7 @@ fn host_run_plugin_command(caller: Caller<'_, PluginEnv>) { PluginCommand::WatchFilesystem => watch_filesystem(env), PluginCommand::DumpSessionLayout => dump_session_layout(env), PluginCommand::CloseSelf => close_self(env), - PluginCommand::RebindKeys(new_keybinds) => rebind_keys(env, new_keybinds)?, + PluginCommand::Reconfigure(new_config) => reconfigure(env, new_config)?, }, (PermissionStatus::Denied, permission) => { log::error!( @@ -781,11 +782,11 @@ fn close_self(env: &PluginEnv) { .non_fatal(); } -fn rebind_keys(env: &PluginEnv, new_keybinds: String) -> Result<()> { - let err_context = || "Failed to rebind keys"; +fn reconfigure(env: &PluginEnv, new_config: String) -> Result<()> { + let err_context = || "Failed to reconfigure"; let client_id = env.client_id; env.senders - .send_to_server(ServerInstruction::RebindKeys(client_id, new_keybinds)) + .send_to_server(ServerInstruction::Reconfigure(client_id, new_config)) .with_context(err_context)?; Ok(()) } @@ -1446,7 +1447,7 @@ fn check_command_permission( | PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes, PluginCommand::MessageToPlugin(..) => PermissionType::MessageAndLaunchOtherPlugins, PluginCommand::DumpSessionLayout => PermissionType::ReadApplicationState, - PluginCommand::RebindKeys(..) => PermissionType::RebindKeys, + PluginCommand::Reconfigure(..) => PermissionType::Reconfigure, _ => return (PermissionStatus::Granted, None), }; |
