aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-server/src/plugins
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2024-07-17 16:08:31 +0200
committerGitHub <noreply@github.com>2024-07-17 16:08:31 +0200
commit84ff29dd024c73e94f3e65d56d907ae903fe3752 (patch)
treeb3f01370b3b1af71f018eeb27a1437b75e959721 /zellij-server/src/plugins
parentdocs(changelog): scroll region fix (diff)
downloadsl0ppij-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')
-rw-r--r--zellij-server/src/plugins/mod.rs16
-rw-r--r--zellij-server/src/plugins/plugin_loader.rs24
-rw-r--r--zellij-server/src/plugins/plugin_map.rs8
-rw-r--r--zellij-server/src/plugins/unit/plugin_tests.rs10
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap4
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__reconfigure_plugin_command.snap11
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap4
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs66
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs11
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),
};