diff --git a/Cargo.toml b/Cargo.toml index 56c855f0..f01e8f90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,3 +74,7 @@ default = [ "zellij-utils/plugins_from_target" ] disable_automatic_asset_installation = [ "zellij-utils/disable_automatic_asset_installation" ] unstable = [ "zellij-client/unstable", "zellij-utils/unstable" ] singlepass = [ "zellij-server/singlepass" ] + +# uncomment this when developing plugins in the Zellij UI to make plugin compilation faster +# [profile.dev.package."*"] +# opt-level = 3 diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap index 59bdf1ff..45d5bc9f 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap @@ -25,5 +25,5 @@ expression: last_snapshot │ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  BASE  + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap index a5e456d2..713fac4d 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap @@ -1,7 +1,7 @@ --- source: src/tests/e2e/cases.rs +assertion_line: 198 expression: last_snapshot - --- Zellij ┌──────┐ @@ -21,5 +21,5 @@ expression: last_snapshot │ │ │ │ └──────┘ - + diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap index 70fa42f4..a0881f67 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap @@ -25,5 +25,5 @@ expression: last_snapshot │ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - Ctrl + LOCK  <> PANE  <> TAB  <> RESIZE  <> MOVE  <> SEARCH  <> SESSION  <> QUIT   BASE  + Ctrl + LOCK  <> PANE  <> TAB  <> RESIZE  <> MOVE  <> SEARCH  <> SESSION  <> QUIT  -- INTERFACE LOCKED -- diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap index b45f0114..29b78eb2 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap @@ -25,5 +25,5 @@ expression: last_snapshot │ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  BASE  + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap index 065bbfe5..2d8a5042 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap @@ -25,5 +25,5 @@ expression: last_snapshot │ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - LOCK  PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT   BASE  + LOCK  PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  Tip: UNBOUND => open new pane. UNBOUND => navigate between panes. UNBOUND => increase/decrease pane size. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap index 76d6ee12..7bc4b49f 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap @@ -25,5 +25,5 @@ expression: last_snapshot │ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  BASE  + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap index 6f84144f..b817373e 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap @@ -25,5 +25,5 @@ expression: last_snapshot │ │ │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  BASE  + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|-> => resize pane. diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index bf0f1a82..f82c9d68 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -764,7 +764,7 @@ fn init_session( Some(&to_screen), Some(&to_pty), Some(&to_plugin), - None, + Some(&to_server), Some(&to_pty_writer), Some(&to_background_jobs), None, diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index b76d21ec..6f44f6eb 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -1254,7 +1254,7 @@ impl Grid { let new_row = Row::new(self.width).canonical(); self.viewport.push(new_row); } - if self.cursor.y == self.height - 1 { + if self.cursor.y == self.height.saturating_sub(1) { if self.scroll_region.is_none() { if self.alternate_screen_state.is_none() { self.transfer_rows_to_lines_above(1); @@ -1406,7 +1406,7 @@ impl Grid { } fn line_wrap(&mut self) { self.cursor.x = 0; - if self.cursor.y == self.height - 1 { + if self.cursor.y == self.height.saturating_sub(1) { if self.alternate_screen_state.is_none() { self.transfer_rows_to_lines_above(1); } else { diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 8a45c3c1..7915069c 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -545,6 +545,12 @@ impl Pane for PluginPane { self.loading_indication.to_string().as_bytes().to_vec(), ); } + fn start_loading_indication(&mut self, loading_indication: LoadingIndication) { + self.loading_indication.merge(loading_indication); + self.handle_plugin_bytes_for_all_clients( + self.loading_indication.to_string().as_bytes().to_vec(), + ); + } fn progress_animation_offset(&mut self) { if self.loading_indication.ended { return; diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 2aca679a..eeb19c20 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -1,11 +1,11 @@ -mod start_plugin; +mod plugin_loader; mod wasm_bridge; use log::info; use std::{collections::HashMap, fs, path::PathBuf}; use wasmer::Store; use crate::screen::ScreenInstruction; -use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId}; +use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction}; use wasm_bridge::WasmBridge; @@ -32,7 +32,15 @@ pub enum PluginInstruction { ), Update(Vec<(Option, Option, Event)>), // Focused plugin / broadcast, client_id, event data Unload(u32), // plugin_id - Resize(u32, usize, usize), // plugin_id, columns, rows + Reload( + Option, // should float + Option, // pane title + RunPlugin, + usize, // tab index + ClientId, + Size, + ), + Resize(u32, usize, usize), // plugin_id, columns, rows AddClient(ClientId), RemoveClient(ClientId), NewTab( @@ -43,7 +51,7 @@ pub enum PluginInstruction { usize, // tab_index ClientId, ), - ApplyCachedEvents(u32), // u32 is the plugin id + ApplyCachedEvents(Vec), // a list of plugin id Exit, } @@ -53,6 +61,7 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::Load(..) => PluginContext::Load, PluginInstruction::Update(..) => PluginContext::Update, PluginInstruction::Unload(..) => PluginContext::Unload, + PluginInstruction::Reload(..) => PluginContext::Reload, PluginInstruction::Resize(..) => PluginContext::Resize, PluginInstruction::Exit => PluginContext::Exit, PluginInstruction::AddClient(_) => PluginContext::AddClient, @@ -103,6 +112,42 @@ pub(crate) fn plugin_thread_main( PluginInstruction::Unload(pid) => { wasm_bridge.unload_plugin(pid)?; }, + PluginInstruction::Reload( + should_float, + pane_title, + run, + tab_index, + client_id, + size, + ) => match wasm_bridge.reload_plugin(&run) { + Ok(_) => { + let _ = bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread); + }, + Err(err) => match err.downcast_ref::() { + Some(ZellijError::PluginDoesNotExist) => { + log::warn!("Plugin {} not found, starting it instead", run.location); + match wasm_bridge.load_plugin(&run, tab_index, size, client_id) { + Ok(plugin_id) => { + drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin( + should_float, + run, + pane_title, + tab_index, + plugin_id, + ))); + }, + Err(e) => { + log::error!("Failed to load plugin: {e}"); + }, + }; + }, + _ => { + return Err(err); + }, + }, + }, PluginInstruction::Resize(pid, new_columns, new_rows) => { wasm_bridge.resize_plugin(pid, new_columns, new_rows)?; }, diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs new file mode 100644 index 00000000..c5413388 --- /dev/null +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -0,0 +1,656 @@ +use crate::plugins::wasm_bridge::{wasi_read_string, zellij_exports, PluginEnv, PluginMap}; +use highway::{HighwayHash, PortableHash}; +use log::info; +use semver::Version; +use std::{ + collections::{HashMap, HashSet}, + fmt, fs, + path::PathBuf, + sync::{Arc, Mutex}, +}; +use url::Url; +use wasmer::{ChainableNamedResolver, Instance, Module, Store}; +use wasmer_wasi::{Pipe, WasiState}; + +use crate::{ + logging_pipe::LoggingPipe, screen::ScreenInstruction, thread_bus::ThreadSenders, + ui::loading_indication::LoadingIndication, ClientId, +}; + +use zellij_utils::{ + consts::{VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR}, + errors::prelude::*, + input::plugins::PluginConfig, + pane_size::Size, +}; + +macro_rules! display_loading_stage { + ($loading_stage:ident, $loading_indication:expr, $senders:expr, $plugin_id:expr) => {{ + $loading_indication.$loading_stage(); + drop( + $senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( + $plugin_id, + $loading_indication.clone(), + )), + ); + }}; +} + +/// Custom error for plugin version mismatch. +/// +/// This is thrown when, during starting a plugin, it is detected that the plugin version doesn't +/// match the zellij version. This is treated as a fatal error and leads to instantaneous +/// termination. +#[derive(Debug)] +pub struct VersionMismatchError { + zellij_version: String, + plugin_version: String, + plugin_path: PathBuf, + // true for builtin plugins + builtin: bool, +} + +impl std::error::Error for VersionMismatchError {} + +impl VersionMismatchError { + pub fn new( + zellij_version: &str, + plugin_version: &str, + plugin_path: &PathBuf, + builtin: bool, + ) -> Self { + VersionMismatchError { + zellij_version: zellij_version.to_owned(), + plugin_version: plugin_version.to_owned(), + plugin_path: plugin_path.to_owned(), + builtin, + } + } +} + +impl fmt::Display for VersionMismatchError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let first_line = if self.builtin { + "It seems your version of zellij was built with outdated core plugins." + } else { + "If you're seeing this error a plugin version doesn't match the current +zellij version." + }; + + write!( + f, + "{} +Detected versions: + +- Plugin version: {} +- Zellij version: {} +- Offending plugin: {} + +If you're a user: + Please contact the distributor of your zellij version and report this error + to them. + +If you're a developer: + Please run zellij with updated plugins. The easiest way to achieve this + is to build zellij with `cargo xtask install`. Also refer to the docs: + https://github.com/zellij-org/zellij/blob/main/CONTRIBUTING.md#building +", + first_line, + self.plugin_version.trim_end(), + self.zellij_version.trim_end(), + self.plugin_path.display() + ) + } +} + +// Returns `Ok` if the plugin version matches the zellij version. +// Returns an `Err` otherwise. +fn assert_plugin_version(instance: &Instance, plugin_env: &PluginEnv) -> Result<()> { + let err_context = || { + format!( + "failed to determine plugin version for plugin {}", + plugin_env.plugin.path.display() + ) + }; + + let plugin_version_func = match instance.exports.get_function("plugin_version") { + Ok(val) => val, + Err(_) => { + return Err(anyError::new(VersionMismatchError::new( + VERSION, + "Unavailable", + &plugin_env.plugin.path, + plugin_env.plugin.is_builtin(), + ))) + }, + }; + + let plugin_version = plugin_version_func + .call(&[]) + .map_err(anyError::new) + .and_then(|_| wasi_read_string(&plugin_env.wasi_env)) + .and_then(|string| Version::parse(&string).context("failed to parse plugin version")) + .with_context(err_context)?; + let zellij_version = Version::parse(VERSION) + .context("failed to parse zellij version") + .with_context(err_context)?; + if plugin_version != zellij_version { + return Err(anyError::new(VersionMismatchError::new( + VERSION, + &plugin_version.to_string(), + &plugin_env.plugin.path, + plugin_env.plugin.is_builtin(), + ))); + } + + Ok(()) +} + +fn load_plugin_instance(instance: &mut Instance) -> Result<()> { + let err_context = || format!("failed to load plugin from instance {instance:#?}"); + + let load_function = instance + .exports + .get_function("_start") + .with_context(err_context)?; + // This eventually calls the `.load()` method + load_function.call(&[]).with_context(err_context)?; + Ok(()) +} + +pub struct PluginLoader<'a> { + plugin_cache: Arc>>, + plugin_map: Arc>, + plugin_path: PathBuf, + loading_indication: &'a mut LoadingIndication, + senders: ThreadSenders, + plugin_id: u32, + client_id: ClientId, + store: Store, + plugin: PluginConfig, + plugin_dir: &'a PathBuf, + tab_index: usize, + plugin_own_data_dir: PathBuf, + size: Size, + wasm_blob_on_hd: Option<(Vec, PathBuf)>, +} + +impl<'a> PluginLoader<'a> { + pub fn reload_plugin_from_memory( + plugin_id: u32, + plugin_dir: PathBuf, + plugin_cache: Arc>>, + senders: ThreadSenders, + store: Store, + plugin_map: Arc>, + connected_clients: Arc>>, + loading_indication: &mut LoadingIndication, + ) -> Result<()> { + let err_context = || format!("failed to reload plugin {plugin_id} from memory"); + let mut connected_clients: Vec = + connected_clients.lock().unwrap().iter().copied().collect(); + if connected_clients.is_empty() { + return Err(anyhow!("No connected clients, cannot reload plugin")); + } + let first_client_id = connected_clients.remove(0); + + let mut plugin_loader = PluginLoader::new_from_existing_plugin_attributes( + &plugin_cache, + &plugin_map, + loading_indication, + &senders, + plugin_id, + first_client_id, + &store, + &plugin_dir, + )?; + plugin_loader + .load_module_from_memory() + .and_then(|module| plugin_loader.create_plugin_instance_and_environment(module)) + .and_then(|(instance, plugin_env)| { + plugin_loader.load_plugin_instance(&instance, &plugin_env)?; + plugin_loader.clone_instance_for_other_clients( + &instance, + &plugin_env, + &connected_clients, + ) + }) + .with_context(err_context)?; + display_loading_stage!(end, loading_indication, senders, plugin_id); + Ok(()) + } + + pub fn start_plugin( + plugin_id: u32, + client_id: ClientId, + plugin: &PluginConfig, + tab_index: usize, + plugin_dir: PathBuf, + plugin_cache: Arc>>, + senders: ThreadSenders, + store: Store, + plugin_map: Arc>, + size: Size, + connected_clients: Arc>>, + loading_indication: &mut LoadingIndication, + ) -> Result<()> { + let err_context = || format!("failed to start plugin {plugin:#?} for client {client_id}"); + let mut plugin_loader = PluginLoader::new( + &plugin_cache, + &plugin_map, + loading_indication, + &senders, + plugin_id, + client_id, + &store, + plugin.clone(), + &plugin_dir, + tab_index, + size, + )?; + plugin_loader + .load_module_from_memory() + .or_else(|_e| plugin_loader.load_module_from_hd_cache()) + .or_else(|_e| plugin_loader.compile_module()) + .and_then(|module| plugin_loader.create_plugin_instance_and_environment(module)) + .and_then(|(instance, plugin_env)| { + plugin_loader.load_plugin_instance(&instance, &plugin_env)?; + plugin_loader.clone_instance_for_other_clients( + &instance, + &plugin_env, + &connected_clients.lock().unwrap(), + ) + }) + .with_context(err_context)?; + display_loading_stage!(end, loading_indication, senders, plugin_id); + Ok(()) + } + + pub fn reload_plugin( + plugin_id: u32, + plugin_dir: PathBuf, + plugin_cache: Arc>>, + senders: ThreadSenders, + store: Store, + plugin_map: Arc>, + connected_clients: Arc>>, + loading_indication: &mut LoadingIndication, + ) -> Result<()> { + let err_context = || format!("failed to reload plugin id {plugin_id}"); + + let mut connected_clients: Vec = + connected_clients.lock().unwrap().iter().copied().collect(); + if connected_clients.is_empty() { + return Err(anyhow!("No connected clients, cannot reload plugin")); + } + let first_client_id = connected_clients.remove(0); + + let mut plugin_loader = PluginLoader::new_from_existing_plugin_attributes( + &plugin_cache, + &plugin_map, + loading_indication, + &senders, + plugin_id, + first_client_id, + &store, + &plugin_dir, + )?; + plugin_loader + .compile_module() + .and_then(|module| plugin_loader.create_plugin_instance_and_environment(module)) + .and_then(|(instance, plugin_env)| { + plugin_loader.load_plugin_instance(&instance, &plugin_env)?; + plugin_loader.clone_instance_for_other_clients( + &instance, + &plugin_env, + &connected_clients, + ) + }) + .with_context(err_context)?; + display_loading_stage!(end, loading_indication, senders, plugin_id); + Ok(()) + } + pub fn new( + plugin_cache: &Arc>>, + plugin_map: &Arc>, + loading_indication: &'a mut LoadingIndication, + senders: &ThreadSenders, + plugin_id: u32, + client_id: ClientId, + store: &Store, + plugin: PluginConfig, + plugin_dir: &'a PathBuf, + tab_index: usize, + size: Size, + ) -> Result { + let plugin_own_data_dir = ZELLIJ_CACHE_DIR.join(Url::from(&plugin.location).to_string()); + create_plugin_fs_entries(&plugin_own_data_dir)?; + let plugin_path = plugin.path.clone(); + Ok(PluginLoader { + plugin_cache: plugin_cache.clone(), + plugin_map: plugin_map.clone(), + plugin_path, + loading_indication, + senders: senders.clone(), + plugin_id, + client_id, + store: store.clone(), + plugin, + plugin_dir, + tab_index, + plugin_own_data_dir, + size, + wasm_blob_on_hd: None, + }) + } + pub fn new_from_existing_plugin_attributes( + plugin_cache: &Arc>>, + plugin_map: &Arc>, + loading_indication: &'a mut LoadingIndication, + senders: &ThreadSenders, + plugin_id: u32, + client_id: ClientId, + store: &Store, + plugin_dir: &'a PathBuf, + ) -> Result { + let err_context = || "Failed to find existing plugin"; + let (_old_instance, old_user_env, (rows, cols)) = { + let mut plugin_map = plugin_map.lock().unwrap(); + plugin_map + .remove(&(plugin_id, client_id)) + .with_context(err_context)? + }; + let tab_index = old_user_env.tab_index; + let size = Size { rows, cols }; + let plugin_config = old_user_env.plugin.clone(); + loading_indication.set_name(old_user_env.name()); + PluginLoader::new( + plugin_cache, + plugin_map, + loading_indication, + senders, + plugin_id, + client_id, + store, + plugin_config, + plugin_dir, + tab_index, + size, + ) + } + pub fn load_module_from_memory(&mut self) -> Result { + display_loading_stage!( + indicate_loading_plugin_from_memory, + self.loading_indication, + self.senders, + self.plugin_id + ); + let module = self + .plugin_cache + .lock() + .unwrap() + .remove(&self.plugin_path) + .ok_or(anyhow!("Plugin is not stored in memory"))?; + display_loading_stage!( + indicate_loading_plugin_from_memory_success, + self.loading_indication, + self.senders, + self.plugin_id + ); + Ok(module) + } + pub fn load_module_from_hd_cache(&mut self) -> Result { + display_loading_stage!( + indicate_loading_plugin_from_memory_notfound, + self.loading_indication, + self.senders, + self.plugin_id + ); + display_loading_stage!( + indicate_loading_plugin_from_hd_cache, + self.loading_indication, + self.senders, + self.plugin_id + ); + let (_wasm_bytes, cached_path) = self.plugin_bytes_and_cache_path()?; + let timer = std::time::Instant::now(); + let module = unsafe { Module::deserialize_from_file(&self.store, &cached_path)? }; + log::info!( + "Loaded plugin '{}' from cache folder at '{}' in {:?}", + self.plugin_path.display(), + ZELLIJ_CACHE_DIR.display(), + timer.elapsed(), + ); + display_loading_stage!( + indicate_loading_plugin_from_hd_cache_success, + self.loading_indication, + self.senders, + self.plugin_id + ); + Ok(module) + } + pub fn compile_module(&mut self) -> Result { + display_loading_stage!( + indicate_loading_plugin_from_hd_cache_notfound, + self.loading_indication, + self.senders, + self.plugin_id + ); + display_loading_stage!( + indicate_compiling_plugin, + self.loading_indication, + self.senders, + self.plugin_id + ); + let (wasm_bytes, cached_path) = self.plugin_bytes_and_cache_path()?; + let timer = std::time::Instant::now(); + let err_context = || "failed to recover cache dir"; + let module = fs::create_dir_all(ZELLIJ_CACHE_DIR.to_owned()) + .map_err(anyError::new) + .and_then(|_| { + // compile module + Module::new(&self.store, &wasm_bytes).map_err(anyError::new) + }) + .and_then(|m| { + // serialize module to HD cache for faster loading in the future + m.serialize_to_file(&cached_path).map_err(anyError::new)?; + log::info!( + "Compiled plugin '{}' in {:?}", + self.plugin_path.display(), + timer.elapsed() + ); + Ok(m) + }) + .with_context(err_context)?; + Ok(module) + } + pub fn create_plugin_instance_and_environment( + &mut self, + module: Module, + ) -> Result<(Instance, PluginEnv)> { + let err_context = || { + format!( + "Failed to create instance and plugin env for plugin {}", + self.plugin_id + ) + }; + let mut wasi_env = WasiState::new("Zellij") + .env("CLICOLOR_FORCE", "1") + .map_dir("/host", ".") + .and_then(|wasi| wasi.map_dir("/data", &self.plugin_own_data_dir)) + .and_then(|wasi| wasi.map_dir("/tmp", ZELLIJ_TMP_DIR.as_path())) + .and_then(|wasi| { + wasi.stdin(Box::new(Pipe::new())) + .stdout(Box::new(Pipe::new())) + .stderr(Box::new(LoggingPipe::new( + &self.plugin.location.to_string(), + self.plugin_id, + ))) + .finalize() + }) + .with_context(err_context)?; + let wasi = wasi_env.import_object(&module).with_context(err_context)?; + + let mut mut_plugin = self.plugin.clone(); + mut_plugin.set_tab_index(self.tab_index); + let plugin_env = PluginEnv { + plugin_id: self.plugin_id, + client_id: self.client_id, + plugin: mut_plugin, + senders: self.senders.clone(), + wasi_env, + subscriptions: Arc::new(Mutex::new(HashSet::new())), + plugin_own_data_dir: self.plugin_own_data_dir.clone(), + tab_index: self.tab_index, + }; + + let zellij = zellij_exports(&self.store, &plugin_env); + let instance = + Instance::new(&module, &zellij.chain_back(wasi)).with_context(err_context)?; + assert_plugin_version(&instance, &plugin_env).with_context(err_context)?; + // Only do an insert when everything went well! + let cloned_plugin = self.plugin.clone(); + self.plugin_cache + .lock() + .unwrap() + .insert(cloned_plugin.path, module); + Ok((instance, plugin_env)) + } + pub fn load_plugin_instance( + &mut self, + instance: &Instance, + plugin_env: &PluginEnv, + ) -> Result<()> { + let err_context = || format!("failed to load plugin from instance {instance:#?}"); + let main_user_instance = instance.clone(); + let main_user_env = plugin_env.clone(); + display_loading_stage!( + indicate_starting_plugin, + self.loading_indication, + self.senders, + self.plugin_id + ); + let load_function = instance + .exports + .get_function("_start") + .with_context(err_context)?; + // This eventually calls the `.load()` method + load_function.call(&[]).with_context(err_context)?; + display_loading_stage!( + indicate_starting_plugin_success, + self.loading_indication, + self.senders, + self.plugin_id + ); + display_loading_stage!( + indicate_writing_plugin_to_cache, + self.loading_indication, + self.senders, + self.plugin_id + ); + let mut plugin_map = self.plugin_map.lock().unwrap(); + plugin_map.insert( + (self.plugin_id, self.client_id), + ( + main_user_instance, + main_user_env, + (self.size.rows, self.size.cols), + ), + ); + display_loading_stage!( + indicate_writing_plugin_to_cache_success, + self.loading_indication, + self.senders, + self.plugin_id + ); + Ok(()) + } + pub fn clone_instance_for_other_clients( + &mut self, + instance: &Instance, + plugin_env: &PluginEnv, + connected_clients: &[ClientId], + ) -> Result<()> { + if !connected_clients.is_empty() { + display_loading_stage!( + indicate_cloning_plugin_for_other_clients, + self.loading_indication, + self.senders, + self.plugin_id + ); + let mut plugin_map = self.plugin_map.lock().unwrap(); + for client_id in connected_clients { + let (instance, new_plugin_env) = + clone_plugin_for_client(&plugin_env, *client_id, &instance, &self.store)?; + plugin_map.insert( + (self.plugin_id, *client_id), + (instance, new_plugin_env, (self.size.rows, self.size.cols)), + ); + } + display_loading_stage!( + indicate_cloning_plugin_for_other_clients_success, + self.loading_indication, + self.senders, + self.plugin_id + ); + } + Ok(()) + } + fn plugin_bytes_and_cache_path(&mut self) -> Result<(Vec, PathBuf)> { + match self.wasm_blob_on_hd.as_ref() { + Some((wasm_bytes, cached_path)) => Ok((wasm_bytes.clone(), cached_path.clone())), + None => { + if self.plugin._allow_exec_host_cmd { + info!( + "Plugin({:?}) is able to run any host command, this may lead to some security issues!", + self.plugin.path + ); + } + // The plugins blob as stored on the filesystem + let wasm_bytes = self.plugin.resolve_wasm_bytes(&self.plugin_dir)?; + let hash: String = PortableHash::default() + .hash256(&wasm_bytes) + .iter() + .map(ToString::to_string) + .collect(); + let cached_path = ZELLIJ_CACHE_DIR.join(&hash); + self.wasm_blob_on_hd = Some((wasm_bytes.clone(), cached_path.clone())); + Ok((wasm_bytes, cached_path)) + }, + } + } +} + +fn create_plugin_fs_entries(plugin_own_data_dir: &PathBuf) -> Result<()> { + let err_context = || "failed to create plugin fs entries"; + // Create filesystem entries mounted into WASM. + // We create them here to get expressive error messages in case they fail. + fs::create_dir_all(&plugin_own_data_dir) + .with_context(|| format!("failed to create datadir in {plugin_own_data_dir:?}")) + .with_context(err_context)?; + fs::create_dir_all(ZELLIJ_TMP_DIR.as_path()) + .with_context(|| format!("failed to create tmpdir at {:?}", &ZELLIJ_TMP_DIR.as_path())) + .with_context(err_context)?; + Ok(()) +} + +fn clone_plugin_for_client( + plugin_env: &PluginEnv, + client_id: ClientId, + instance: &Instance, + store: &Store, +) -> Result<(Instance, PluginEnv)> { + let err_context = || format!("Failed to clone plugin for client {client_id}"); + let mut new_plugin_env = plugin_env.clone(); + new_plugin_env.client_id = client_id; + let module = instance.module().clone(); + let wasi = new_plugin_env + .wasi_env + .import_object(&module) + .with_context(err_context)?; + let zellij = zellij_exports(store, &new_plugin_env); + let mut instance = + Instance::new(&module, &zellij.chain_back(wasi)).with_context(err_context)?; + load_plugin_instance(&mut instance).with_context(err_context)?; + Ok((instance, new_plugin_env)) +} diff --git a/zellij-server/src/plugins/start_plugin.rs b/zellij-server/src/plugins/start_plugin.rs deleted file mode 100644 index 28000bd1..00000000 --- a/zellij-server/src/plugins/start_plugin.rs +++ /dev/null @@ -1,471 +0,0 @@ -use crate::plugins::wasm_bridge::{wasi_read_string, zellij_exports, PluginEnv, PluginMap}; -use highway::{HighwayHash, PortableHash}; -use log::info; -use semver::Version; -use std::{ - collections::{HashMap, HashSet}, - fmt, fs, - path::PathBuf, - sync::{Arc, Mutex}, - time::Instant, -}; -use url::Url; -use wasmer::{ChainableNamedResolver, Instance, Module, Store}; -use wasmer_wasi::{Pipe, WasiState}; - -use crate::{ - logging_pipe::LoggingPipe, screen::ScreenInstruction, thread_bus::ThreadSenders, - ui::loading_indication::LoadingIndication, ClientId, -}; - -use zellij_utils::{ - consts::{VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR}, - errors::prelude::*, - input::plugins::PluginConfig, - pane_size::Size, -}; - -/// Custom error for plugin version mismatch. -/// -/// This is thrown when, during starting a plugin, it is detected that the plugin version doesn't -/// match the zellij version. This is treated as a fatal error and leads to instantaneous -/// termination. -#[derive(Debug)] -pub struct VersionMismatchError { - zellij_version: String, - plugin_version: String, - plugin_path: PathBuf, - // true for builtin plugins - builtin: bool, -} - -impl std::error::Error for VersionMismatchError {} - -impl VersionMismatchError { - pub fn new( - zellij_version: &str, - plugin_version: &str, - plugin_path: &PathBuf, - builtin: bool, - ) -> Self { - VersionMismatchError { - zellij_version: zellij_version.to_owned(), - plugin_version: plugin_version.to_owned(), - plugin_path: plugin_path.to_owned(), - builtin, - } - } -} - -impl fmt::Display for VersionMismatchError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let first_line = if self.builtin { - "It seems your version of zellij was built with outdated core plugins." - } else { - "If you're seeing this error a plugin version doesn't match the current -zellij version." - }; - - write!( - f, - "{} -Detected versions: - -- Plugin version: {} -- Zellij version: {} -- Offending plugin: {} - -If you're a user: - Please contact the distributor of your zellij version and report this error - to them. - -If you're a developer: - Please run zellij with updated plugins. The easiest way to achieve this - is to build zellij with `cargo xtask install`. Also refer to the docs: - https://github.com/zellij-org/zellij/blob/main/CONTRIBUTING.md#building -", - first_line, - self.plugin_version.trim_end(), - self.zellij_version.trim_end(), - self.plugin_path.display() - ) - } -} - -// Returns `Ok` if the plugin version matches the zellij version. -// Returns an `Err` otherwise. -fn assert_plugin_version(instance: &Instance, plugin_env: &PluginEnv) -> Result<()> { - let err_context = || { - format!( - "failed to determine plugin version for plugin {}", - plugin_env.plugin.path.display() - ) - }; - - let plugin_version_func = match instance.exports.get_function("plugin_version") { - Ok(val) => val, - Err(_) => { - return Err(anyError::new(VersionMismatchError::new( - VERSION, - "Unavailable", - &plugin_env.plugin.path, - plugin_env.plugin.is_builtin(), - ))) - }, - }; - - let plugin_version = plugin_version_func - .call(&[]) - .map_err(anyError::new) - .and_then(|_| wasi_read_string(&plugin_env.wasi_env)) - .and_then(|string| Version::parse(&string).context("failed to parse plugin version")) - .with_context(err_context)?; - let zellij_version = Version::parse(VERSION) - .context("failed to parse zellij version") - .with_context(err_context)?; - if plugin_version != zellij_version { - return Err(anyError::new(VersionMismatchError::new( - VERSION, - &plugin_version.to_string(), - &plugin_env.plugin.path, - plugin_env.plugin.is_builtin(), - ))); - } - - Ok(()) -} - -fn load_plugin_instance(instance: &mut Instance) -> Result<()> { - let err_context = || format!("failed to load plugin from instance {instance:#?}"); - - let load_function = instance - .exports - .get_function("_start") - .with_context(err_context)?; - // This eventually calls the `.load()` method - load_function.call(&[]).with_context(err_context)?; - Ok(()) -} - -pub fn start_plugin( - plugin_id: u32, - client_id: ClientId, - plugin: &PluginConfig, - tab_index: usize, - plugin_dir: PathBuf, - plugin_cache: Arc>>, - senders: ThreadSenders, - mut store: Store, - plugin_map: Arc>, - size: Size, - connected_clients: Arc>>, - loading_indication: &mut LoadingIndication, -) -> Result<()> { - let err_context = || format!("failed to start plugin {plugin:#?} for client {client_id}"); - let plugin_own_data_dir = ZELLIJ_CACHE_DIR.join(Url::from(&plugin.location).to_string()); - create_plugin_fs_entries(&plugin_own_data_dir)?; - - loading_indication.indicate_loading_plugin_from_memory(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - let (module, cache_hit) = { - let mut plugin_cache = plugin_cache.lock().unwrap(); - let (module, cache_hit) = load_module_from_memory(&mut *plugin_cache, &plugin.path); - (module, cache_hit) - }; - - let module = match module { - Some(module) => { - loading_indication.indicate_loading_plugin_from_memory_success(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - module - }, - None => { - loading_indication.indicate_loading_plugin_from_memory_notfound(); - loading_indication.indicate_loading_plugin_from_hd_cache(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - - let (wasm_bytes, cached_path) = plugin_bytes_and_cache_path(&plugin, &plugin_dir); - let timer = std::time::Instant::now(); - match load_module_from_hd_cache(&mut store, &plugin.path, &timer, &cached_path) { - Ok(module) => { - loading_indication.indicate_loading_plugin_from_hd_cache_success(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - module - }, - Err(_e) => { - loading_indication.indicate_loading_plugin_from_hd_cache_notfound(); - loading_indication.indicate_compiling_plugin(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - let module = - compile_module(&mut store, &plugin.path, &timer, &cached_path, wasm_bytes)?; - loading_indication.indicate_compiling_plugin_success(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - module - }, - } - }, - }; - - let (instance, plugin_env) = create_plugin_instance_and_environment( - plugin_id, - client_id, - plugin, - &module, - tab_index, - plugin_own_data_dir, - senders.clone(), - &mut store, - )?; - - if !cache_hit { - // Check plugin version - assert_plugin_version(&instance, &plugin_env).with_context(err_context)?; - } - - // Only do an insert when everything went well! - let cloned_plugin = plugin.clone(); - { - let mut plugin_cache = plugin_cache.lock().unwrap(); - plugin_cache.insert(cloned_plugin.path, module); - } - - let mut main_user_instance = instance.clone(); - let main_user_env = plugin_env.clone(); - loading_indication.indicate_starting_plugin(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - load_plugin_instance(&mut main_user_instance).with_context(err_context)?; - loading_indication.indicate_starting_plugin_success(); - loading_indication.indicate_writing_plugin_to_cache(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - - { - let mut plugin_map = plugin_map.lock().unwrap(); - plugin_map.insert( - (plugin_id, client_id), - (main_user_instance, main_user_env, (size.rows, size.cols)), - ); - } - - loading_indication.indicate_writing_plugin_to_cache_success(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - - let connected_clients: Vec = - connected_clients.lock().unwrap().iter().copied().collect(); - if !connected_clients.is_empty() { - loading_indication.indicate_cloning_plugin_for_other_clients(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - let mut plugin_map = plugin_map.lock().unwrap(); - for client_id in connected_clients { - let (instance, new_plugin_env) = - clone_plugin_for_client(&plugin_env, client_id, &instance, &mut store)?; - plugin_map.insert( - (plugin_id, client_id), - (instance, new_plugin_env, (size.rows, size.cols)), - ); - } - loading_indication.indicate_cloning_plugin_for_other_clients_success(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - } - loading_indication.end(); - let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - Ok(()) -} - -fn create_plugin_fs_entries(plugin_own_data_dir: &PathBuf) -> Result<()> { - let err_context = || "failed to create plugin fs entries"; - // Create filesystem entries mounted into WASM. - // We create them here to get expressive error messages in case they fail. - fs::create_dir_all(&plugin_own_data_dir) - .with_context(|| format!("failed to create datadir in {plugin_own_data_dir:?}")) - .with_context(err_context)?; - fs::create_dir_all(ZELLIJ_TMP_DIR.as_path()) - .with_context(|| format!("failed to create tmpdir at {:?}", &ZELLIJ_TMP_DIR.as_path())) - .with_context(err_context)?; - Ok(()) -} - -fn compile_module( - store: &mut Store, - plugin_path: &PathBuf, - timer: &Instant, - cached_path: &PathBuf, - wasm_bytes: Vec, -) -> Result { - let err_context = || "failed to recover cache dir"; - fs::create_dir_all(ZELLIJ_CACHE_DIR.to_owned()) - .map_err(anyError::new) - .and_then(|_| { - // compile module - Module::new(&*store, &wasm_bytes).map_err(anyError::new) - }) - .map(|m| { - // serialize module to HD cache for faster loading in the future - m.serialize_to_file(&cached_path).map_err(anyError::new)?; - log::info!( - "Compiled plugin '{}' in {:?}", - plugin_path.display(), - timer.elapsed() - ); - Ok(m) - }) - .with_context(err_context)? -} - -fn load_module_from_hd_cache( - store: &mut Store, - plugin_path: &PathBuf, - timer: &Instant, - cached_path: &PathBuf, -) -> Result { - let module = unsafe { Module::deserialize_from_file(&*store, &cached_path)? }; - log::info!( - "Loaded plugin '{}' from cache folder at '{}' in {:?}", - plugin_path.display(), - ZELLIJ_CACHE_DIR.display(), - timer.elapsed(), - ); - Ok(module) -} - -fn plugin_bytes_and_cache_path(plugin: &PluginConfig, plugin_dir: &PathBuf) -> (Vec, PathBuf) { - let err_context = || "failed to get plugin bytes and cached path"; - // Populate plugin module cache for this plugin! - // Is it in the cache folder already? - if plugin._allow_exec_host_cmd { - info!( - "Plugin({:?}) is able to run any host command, this may lead to some security issues!", - plugin.path - ); - } - // The plugins blob as stored on the filesystem - let wasm_bytes = plugin - .resolve_wasm_bytes(&plugin_dir) - .with_context(err_context) - .fatal(); - let hash: String = PortableHash::default() - .hash256(&wasm_bytes) - .iter() - .map(ToString::to_string) - .collect(); - let cached_path = ZELLIJ_CACHE_DIR.join(&hash); - (wasm_bytes, cached_path) -} - -fn load_module_from_memory( - plugin_cache: &mut HashMap, - plugin_path: &PathBuf, -) -> (Option, bool) { - let module = plugin_cache.remove(plugin_path); - let mut cache_hit = false; - if module.is_some() { - cache_hit = true; - log::debug!( - "Loaded plugin '{}' from plugin cache", - plugin_path.display() - ); - } - (module, cache_hit) -} - -fn create_plugin_instance_and_environment( - plugin_id: u32, - client_id: ClientId, - plugin: &PluginConfig, - module: &Module, - tab_index: usize, - plugin_own_data_dir: PathBuf, - senders: ThreadSenders, - store: &mut Store, -) -> Result<(Instance, PluginEnv)> { - let err_context = || format!("Failed to create instance and plugin env for plugin {plugin_id}"); - let mut wasi_env = WasiState::new("Zellij") - .env("CLICOLOR_FORCE", "1") - .map_dir("/host", ".") - .and_then(|wasi| wasi.map_dir("/data", &plugin_own_data_dir)) - .and_then(|wasi| wasi.map_dir("/tmp", ZELLIJ_TMP_DIR.as_path())) - .and_then(|wasi| { - wasi.stdin(Box::new(Pipe::new())) - .stdout(Box::new(Pipe::new())) - .stderr(Box::new(LoggingPipe::new( - &plugin.location.to_string(), - plugin_id, - ))) - .finalize() - }) - .with_context(err_context)?; - let wasi = wasi_env.import_object(&module).with_context(err_context)?; - - let mut mut_plugin = plugin.clone(); - mut_plugin.set_tab_index(tab_index); - let plugin_env = PluginEnv { - plugin_id, - client_id, - plugin: mut_plugin, - senders: senders.clone(), - wasi_env, - subscriptions: Arc::new(Mutex::new(HashSet::new())), - plugin_own_data_dir, - tab_index, - }; - - let zellij = zellij_exports(&store, &plugin_env); - let instance = Instance::new(&module, &zellij.chain_back(wasi)).with_context(err_context)?; - Ok((instance, plugin_env)) -} - -fn clone_plugin_for_client( - plugin_env: &PluginEnv, - client_id: ClientId, - instance: &Instance, - store: &Store, -) -> Result<(Instance, PluginEnv)> { - let err_context = || format!("Failed to clone plugin for client {client_id}"); - let mut new_plugin_env = plugin_env.clone(); - new_plugin_env.client_id = client_id; - let module = instance.module().clone(); - let wasi = new_plugin_env - .wasi_env - .import_object(&module) - .with_context(err_context)?; - let zellij = zellij_exports(store, &new_plugin_env); - let mut instance = - Instance::new(&module, &zellij.chain_back(wasi)).with_context(err_context)?; - load_plugin_instance(&mut instance).with_context(err_context)?; - Ok((instance, new_plugin_env)) -} diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index 68f8bc6d..119a1d24 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -1,10 +1,10 @@ use super::PluginInstruction; -use crate::plugins::start_plugin::start_plugin; +use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError}; use log::{debug, info, warn}; use serde::{de::DeserializeOwned, Serialize}; use std::{ collections::{HashMap, HashSet}, - fmt, + fmt::Display, path::PathBuf, process, str::FromStr, @@ -33,81 +33,17 @@ use zellij_utils::{ consts::VERSION, data::{Event, EventType, PluginIds}, errors::prelude::*, + errors::ZellijError, input::{ command::TerminalAction, - layout::RunPlugin, + layout::{RunPlugin, RunPluginLocation}, plugins::{PluginConfig, PluginType, PluginsConfig}, }, pane_size::Size, serde, }; -/// Custom error for plugin version mismatch. -/// -/// This is thrown when, during starting a plugin, it is detected that the plugin version doesn't -/// match the zellij version. This is treated as a fatal error and leads to instantaneous -/// termination. -#[derive(Debug)] -pub struct VersionMismatchError { - zellij_version: String, - plugin_version: String, - plugin_path: PathBuf, - // true for builtin plugins - builtin: bool, -} - -impl std::error::Error for VersionMismatchError {} - -impl VersionMismatchError { - pub fn new( - zellij_version: &str, - plugin_version: &str, - plugin_path: &PathBuf, - builtin: bool, - ) -> Self { - VersionMismatchError { - zellij_version: zellij_version.to_owned(), - plugin_version: plugin_version.to_owned(), - plugin_path: plugin_path.to_owned(), - builtin, - } - } -} - -impl fmt::Display for VersionMismatchError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let first_line = if self.builtin { - "It seems your version of zellij was built with outdated core plugins." - } else { - "If you're seeing this error a plugin version doesn't match the current -zellij version." - }; - - write!( - f, - "{} -Detected versions: - -- Plugin version: {} -- Zellij version: {} -- Offending plugin: {} - -If you're a user: - Please contact the distributor of your zellij version and report this error - to them. - -If you're a developer: - Please run zellij with updated plugins. The easiest way to achieve this - is to build zellij with `cargo xtask install`. Also refer to the docs: - https://github.com/zellij-org/zellij/blob/main/CONTRIBUTING.md#building -", - first_line, - self.plugin_version.trim_end(), - self.zellij_version.trim_end(), - self.plugin_path.display() - ) - } -} +type PluginId = u32; #[derive(WasmerEnv, Clone)] pub struct PluginEnv { @@ -150,7 +86,8 @@ pub struct WasmBridge { next_plugin_id: u32, cached_events_for_pending_plugins: HashMap>, // u32 is the plugin id cached_resizes_for_pending_plugins: HashMap, // (rows, columns) - loading_plugins: HashMap>, // plugin_id to join-handle + loading_plugins: HashMap<(u32, RunPlugin), JoinHandle<()>>, // plugin_id to join-handle + pending_plugin_reloads: HashSet, } impl WasmBridge { @@ -176,6 +113,7 @@ impl WasmBridge { cached_events_for_pending_plugins: HashMap::new(), cached_resizes_for_pending_plugins: HashMap::new(), loading_plugins: HashMap::new(), + pending_plugin_reloads: HashSet::new(), } } pub fn load_plugin( @@ -196,12 +134,10 @@ impl WasmBridge { .with_context(err_context)?; let plugin_name = run.location.to_string(); - self.next_plugin_id += 1; - self.cached_events_for_pending_plugins .insert(plugin_id, vec![]); self.cached_resizes_for_pending_plugins - .insert(plugin_id, (0, 0)); + .insert(plugin_id, (size.rows, size.cols)); let load_plugin_task = task::spawn({ let plugin_dir = self.plugin_dir.clone(); @@ -214,7 +150,7 @@ impl WasmBridge { let _ = senders.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id)); let mut loading_indication = LoadingIndication::new(plugin_name.clone()); - match start_plugin( + match PluginLoader::start_plugin( plugin_id, client_id, &plugin, @@ -228,30 +164,20 @@ impl WasmBridge { connected_clients.clone(), &mut loading_indication, ) { - Ok(_) => { - let _ = senders.send_to_background_jobs( - BackgroundJob::StopPluginLoadingAnimation(plugin_id), - ); - let _ = - senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(plugin_id)); - }, - Err(e) => { - let _ = senders.send_to_background_jobs( - BackgroundJob::StopPluginLoadingAnimation(plugin_id), - ); - let _ = - senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(plugin_id)); - loading_indication.indicate_loading_error(e.to_string()); - let _ = - senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( - plugin_id, - loading_indication.clone(), - )); - }, + Ok(_) => handle_plugin_successful_loading(&senders, plugin_id), + Err(e) => handle_plugin_loading_failure( + &senders, + plugin_id, + &mut loading_indication, + e, + ), } + let _ = + senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(vec![plugin_id])); } }); - self.loading_plugins.insert(plugin_id, load_plugin_task); + self.loading_plugins + .insert((plugin_id, run.clone()), load_plugin_task); self.next_plugin_id += 1; Ok(plugin_id) } @@ -267,6 +193,89 @@ impl WasmBridge { } Ok(()) } + pub fn reload_plugin(&mut self, run_plugin: &RunPlugin) -> Result<()> { + if self.plugin_is_currently_being_loaded(&run_plugin.location) { + self.pending_plugin_reloads.insert(run_plugin.clone()); + return Ok(()); + } + + let plugin_ids = self.all_plugin_ids_for_plugin_location(&run_plugin.location)?; + for plugin_id in &plugin_ids { + let (rows, columns) = self.size_of_plugin_id(*plugin_id).unwrap_or((0, 0)); + self.cached_events_for_pending_plugins + .insert(*plugin_id, vec![]); + self.cached_resizes_for_pending_plugins + .insert(*plugin_id, (rows, columns)); + } + + let first_plugin_id = *plugin_ids.get(0).unwrap(); // this is safe becaise the above + // methods always returns at least 1 id + let mut loading_indication = LoadingIndication::new(run_plugin.location.to_string()); + self.start_plugin_loading_indication(&plugin_ids, &loading_indication); + let load_plugin_task = task::spawn({ + let plugin_dir = self.plugin_dir.clone(); + let plugin_cache = self.plugin_cache.clone(); + let senders = self.senders.clone(); + let store = self.store.clone(); + let plugin_map = self.plugin_map.clone(); + let connected_clients = self.connected_clients.clone(); + async move { + match PluginLoader::reload_plugin( + first_plugin_id, + plugin_dir.clone(), + plugin_cache.clone(), + senders.clone(), + store.clone(), + plugin_map.clone(), + connected_clients.clone(), + &mut loading_indication, + ) { + Ok(_) => { + handle_plugin_successful_loading(&senders, first_plugin_id); + for plugin_id in &plugin_ids { + if plugin_id == &first_plugin_id { + // no need to reload the plugin we just reloaded + continue; + } + let mut loading_indication = LoadingIndication::new("".into()); + match PluginLoader::reload_plugin_from_memory( + *plugin_id, + plugin_dir.clone(), + plugin_cache.clone(), + senders.clone(), + store.clone(), + plugin_map.clone(), + connected_clients.clone(), + &mut loading_indication, + ) { + Ok(_) => handle_plugin_successful_loading(&senders, *plugin_id), + Err(e) => handle_plugin_loading_failure( + &senders, + *plugin_id, + &mut loading_indication, + e, + ), + } + } + }, + Err(e) => { + for plugin_id in &plugin_ids { + handle_plugin_loading_failure( + &senders, + *plugin_id, + &mut loading_indication, + &e, + ); + } + }, + } + let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(plugin_ids)); + } + }); + self.loading_plugins + .insert((first_plugin_id, run_plugin.clone()), load_plugin_task); + Ok(()) + } pub fn add_client(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to add plugins for client {client_id}"); @@ -311,6 +320,12 @@ impl WasmBridge { for ((plugin_id, client_id), (instance, plugin_env, (current_rows, current_columns))) in plugin_map.iter_mut() { + if self + .cached_resizes_for_pending_plugins + .contains_key(&plugin_id) + { + continue; + } if *plugin_id == pid { *current_rows = new_rows; *current_columns = new_columns; @@ -334,12 +349,10 @@ impl WasmBridge { plugin_bytes.push((*plugin_id, *client_id, rendered_bytes.as_bytes().to_vec())); } } - for (plugin_id, (current_rows, current_columns)) in - self.cached_resizes_for_pending_plugins.iter_mut() - { + for (plugin_id, mut current_size) in self.cached_resizes_for_pending_plugins.iter_mut() { if *plugin_id == pid { - *current_rows = new_rows; - *current_columns = new_columns; + current_size.0 = new_rows; + current_size.1 = new_columns; } } let _ = self @@ -357,6 +370,12 @@ impl WasmBridge { let mut plugin_bytes = vec![]; for (pid, cid, event) in updates.drain(..) { for (&(plugin_id, client_id), (instance, plugin_env, (rows, columns))) in &*plugin_map { + if self + .cached_events_for_pending_plugins + .contains_key(&plugin_id) + { + continue; + } let subs = plugin_env .subscriptions .lock() @@ -394,8 +413,42 @@ impl WasmBridge { .send_to_screen(ScreenInstruction::PluginBytes(plugin_bytes)); Ok(()) } - pub fn apply_cached_events(&mut self, plugin_id: u32) -> Result<()> { - let err_context = || format!("Failed to apply cached events to plugin {plugin_id}"); + pub fn apply_cached_events(&mut self, plugin_ids: Vec) -> Result<()> { + let mut applied_plugin_paths = HashSet::new(); + for plugin_id in plugin_ids { + self.apply_cached_events_and_resizes_for_plugin(plugin_id)?; + if let Some(run_plugin) = self.run_plugin_of_plugin_id(plugin_id) { + applied_plugin_paths.insert(run_plugin.clone()); + } + self.loading_plugins + .retain(|(p_id, _run_plugin), _| p_id != &plugin_id); + } + for run_plugin in applied_plugin_paths.drain() { + if self.pending_plugin_reloads.remove(&run_plugin) { + let _ = self.reload_plugin(&run_plugin); + } + } + Ok(()) + } + pub fn remove_client(&mut self, client_id: ClientId) { + self.connected_clients + .lock() + .unwrap() + .retain(|c| c != &client_id); + } + pub fn cleanup(&mut self) { + for (_plugin_id, loading_plugin_task) in self.loading_plugins.drain() { + drop(loading_plugin_task.cancel()); + } + } + fn run_plugin_of_plugin_id(&self, plugin_id: PluginId) -> Option<&RunPlugin> { + self.loading_plugins + .iter() + .find(|((p_id, _run_plugin), _)| p_id == &plugin_id) + .map(|((_p_id, run_plugin), _)| run_plugin) + } + fn apply_cached_events_and_resizes_for_plugin(&mut self, plugin_id: PluginId) -> Result<()> { + let err_context = || format!("Failed to apply cached events to plugin"); if let Some(events) = self.cached_events_for_pending_plugins.remove(&plugin_id) { let mut plugin_map = self.plugin_map.lock().unwrap(); let all_connected_clients: Vec = self @@ -405,10 +458,10 @@ impl WasmBridge { .iter() .copied() .collect(); - for client_id in all_connected_clients { + for client_id in &all_connected_clients { let mut plugin_bytes = vec![]; if let Some((instance, plugin_env, (rows, columns))) = - plugin_map.get_mut(&(plugin_id, client_id)) + plugin_map.get_mut(&(plugin_id, *client_id)) { let subs = plugin_env .subscriptions @@ -423,7 +476,7 @@ impl WasmBridge { } apply_event_to_plugin( plugin_id, - client_id, + *client_id, &instance, &plugin_env, &event, @@ -441,22 +494,83 @@ impl WasmBridge { if let Some((rows, columns)) = self.cached_resizes_for_pending_plugins.remove(&plugin_id) { self.resize_plugin(plugin_id, columns, rows)?; } - self.loading_plugins.remove(&plugin_id); Ok(()) } - pub fn remove_client(&mut self, client_id: ClientId) { - self.connected_clients + fn plugin_is_currently_being_loaded(&self, plugin_location: &RunPluginLocation) -> bool { + self.loading_plugins + .iter() + .find(|((_plugin_id, run_plugin), _)| &run_plugin.location == plugin_location) + .is_some() + } + fn all_plugin_ids_for_plugin_location( + &self, + plugin_location: &RunPluginLocation, + ) -> Result> { + let err_context = || format!("Failed to get plugin ids for location {plugin_location}"); + let plugin_ids: Vec = self + .plugin_map .lock() .unwrap() - .retain(|c| c != &client_id); + .iter() + .filter( + |((_plugin_id, _client_id), (_instance, plugin_env, _size))| { + &plugin_env.plugin.location == plugin_location + }, + ) + .map(|((plugin_id, _client_id), _)| *plugin_id) + .collect(); + if plugin_ids.is_empty() { + return Err(ZellijError::PluginDoesNotExist).with_context(err_context); + } + Ok(plugin_ids) } - pub fn cleanup(&mut self) { - for (_plugin_id, loading_plugin_task) in self.loading_plugins.drain() { - drop(loading_plugin_task.cancel()); + fn size_of_plugin_id(&self, plugin_id: PluginId) -> Option<(usize, usize)> { + // (rows/colums) + self.plugin_map + .lock() + .unwrap() + .iter() + .find(|((p_id, _client_id), (_instance, _plugin_env, _size))| *p_id == plugin_id) + .map(|((_p_id, _client_id), (_instance, _plugin_env, size))| *size) + } + fn start_plugin_loading_indication( + &self, + plugin_ids: &[PluginId], + loading_indication: &LoadingIndication, + ) { + for plugin_id in plugin_ids { + let _ = self + .senders + .send_to_screen(ScreenInstruction::StartPluginLoadingIndication( + *plugin_id, + loading_indication.clone(), + )); + let _ = self + .senders + .send_to_background_jobs(BackgroundJob::AnimatePluginLoading(*plugin_id)); } } } +fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) { + let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id)); + let _ = senders.send_to_screen(ScreenInstruction::RequestStateUpdateForPlugins); +} + +fn handle_plugin_loading_failure( + senders: &ThreadSenders, + plugin_id: PluginId, + loading_indication: &mut LoadingIndication, + error: impl Display, +) { + let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id)); + loading_indication.indicate_loading_error(error.to_string()); + let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( + plugin_id, + loading_indication.clone(), + )); +} + fn load_plugin_instance(instance: &mut Instance) -> Result<()> { let err_context = || format!("failed to load plugin from instance {instance:#?}"); diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index c14d0c14..cf56a3b1 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -16,6 +16,7 @@ use zellij_utils::{ actions::{Action, SearchDirection, SearchOption}, command::TerminalAction, get_mode_info, + layout::RunPluginLocation, }, ipc::{ClientToServerMsg, ExitReason, IpcReceiverWithContext, ServerToClientMsg}, }; @@ -690,6 +691,18 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, + Action::StartOrReloadPlugin(url) => { + let run_plugin_location = + RunPluginLocation::parse(url.as_str()).with_context(err_context)?; + session + .senders + .send_to_screen(ScreenInstruction::StartOrReloadPluginPane( + run_plugin_location, + None, + client_id, + )) + .with_context(err_context)?; + }, } Ok(should_break) } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 8b5cc5d6..7c698d65 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -258,7 +258,9 @@ pub enum ScreenInstruction { NextSwapLayout(ClientId), QueryTabNames(ClientId), NewTiledPluginPane(RunPluginLocation, Option, ClientId), // Option is + // optional pane title NewFloatingPluginPane(RunPluginLocation, Option, ClientId), // Option is an + StartOrReloadPluginPane(RunPluginLocation, Option, ClientId), // Option is // optional pane title AddPlugin( Option, // should_float @@ -268,7 +270,9 @@ pub enum ScreenInstruction { u32, // plugin id ), UpdatePluginLoadingStage(u32, LoadingIndication), // u32 - plugin_id + StartPluginLoadingIndication(u32, LoadingIndication), // u32 - plugin_id ProgressPluginLoadingOffset(u32), // u32 - plugin id + RequestStateUpdateForPlugins, } impl From<&ScreenInstruction> for ScreenContext { @@ -415,6 +419,9 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::QueryTabNames(..) => ScreenContext::QueryTabNames, ScreenInstruction::NewTiledPluginPane(..) => ScreenContext::NewTiledPluginPane, ScreenInstruction::NewFloatingPluginPane(..) => ScreenContext::NewFloatingPluginPane, + ScreenInstruction::StartOrReloadPluginPane(..) => { + ScreenContext::StartOrReloadPluginPane + }, ScreenInstruction::AddPlugin(..) => ScreenContext::AddPlugin, ScreenInstruction::UpdatePluginLoadingStage(..) => { ScreenContext::UpdatePluginLoadingStage @@ -422,6 +429,12 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ProgressPluginLoadingOffset(..) => { ScreenContext::ProgressPluginLoadingOffset }, + ScreenInstruction::StartPluginLoadingIndication(..) => { + ScreenContext::StartPluginLoadingIndication + }, + ScreenInstruction::RequestStateUpdateForPlugins => { + ScreenContext::RequestStateUpdateForPlugins + }, } } } @@ -2515,6 +2528,30 @@ pub(crate) fn screen_thread_main( size, ))?; }, + ScreenInstruction::StartOrReloadPluginPane( + run_plugin_location, + pane_title, + client_id, + ) => { + let tab_index = screen.active_tab_indices.values().next().unwrap_or(&1); + let size = Size::default(); + let should_float = Some(false); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: run_plugin_location, + }; + screen + .bus + .senders + .send_to_plugin(PluginInstruction::Reload( + should_float, + pane_title, + run_plugin, + *tab_index, + client_id, + size, + ))?; + }, ScreenInstruction::AddPlugin( should_float, run_plugin_location, @@ -2548,6 +2585,16 @@ pub(crate) fn screen_thread_main( } screen.render()?; }, + ScreenInstruction::StartPluginLoadingIndication(pid, loading_indication) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_plugin(pid) { + tab.start_plugin_loading_indication(pid, loading_indication); + break; + } + } + screen.render()?; + }, ScreenInstruction::ProgressPluginLoadingOffset(pid) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { @@ -2558,6 +2605,14 @@ pub(crate) fn screen_thread_main( } screen.render()?; }, + ScreenInstruction::RequestStateUpdateForPlugins => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + tab.update_input_modes()?; + } + screen.update_tabs()?; + screen.render()?; + }, } } Ok(()) diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 65228b83..4a37a090 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -443,6 +443,7 @@ pub trait Pane { fn invoked_with(&self) -> &Option; fn set_title(&mut self, title: String); fn update_loading_indication(&mut self, _loading_indication: LoadingIndication) {} // only relevant for plugins + fn start_loading_indication(&mut self, _loading_indication: LoadingIndication) {} // only relevant for plugins fn progress_animation_offset(&mut self) {} // only relevant for plugins } @@ -3381,6 +3382,24 @@ impl Tab { plugin_pane.update_loading_indication(loading_indication); } } + pub fn start_plugin_loading_indication( + &mut self, + pid: u32, + loading_indication: LoadingIndication, + ) { + if let Some(plugin_pane) = self + .tiled_panes + .get_pane_mut(PaneId::Plugin(pid)) + .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) + .or_else(|| { + self.suppressed_panes + .values_mut() + .find(|s_p| s_p.pid() == PaneId::Plugin(pid)) + }) + { + plugin_pane.start_loading_indication(loading_indication); + } + } pub fn progress_plugin_loading_offset(&mut self, pid: u32) { if let Some(plugin_pane) = self .tiled_panes diff --git a/zellij-server/src/ui/loading_indication.rs b/zellij-server/src/ui/loading_indication.rs index 26bf299d..78af2c10 100644 --- a/zellij-server/src/ui/loading_indication.rs +++ b/zellij-server/src/ui/loading_indication.rs @@ -35,6 +35,9 @@ impl LoadingIndication { ..Default::default() } } + pub fn set_name(&mut self, plugin_name: String) { + self.plugin_name = plugin_name; + } pub fn with_colors(mut self, terminal_emulator_colors: Palette) -> Self { self.terminal_emulator_colors = Some(terminal_emulator_colors); self diff --git a/zellij-utils/assets/compact-bar.wasm b/zellij-utils/assets/compact-bar.wasm old mode 100644 new mode 100755 index 46267f5e..00d14d0e Binary files a/zellij-utils/assets/compact-bar.wasm and b/zellij-utils/assets/compact-bar.wasm differ diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm index 00d14d0e..0e3598d0 100755 Binary files a/zellij-utils/assets/plugins/compact-bar.wasm and b/zellij-utils/assets/plugins/compact-bar.wasm differ diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm index 66e6b10d..a1d0c817 100755 Binary files a/zellij-utils/assets/plugins/status-bar.wasm and b/zellij-utils/assets/plugins/status-bar.wasm differ diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm index 287385a5..37da81bc 100755 Binary files a/zellij-utils/assets/plugins/strider.wasm and b/zellij-utils/assets/plugins/strider.wasm differ diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm index b22bcf10..38f17dfc 100755 Binary files a/zellij-utils/assets/plugins/tab-bar.wasm and b/zellij-utils/assets/plugins/tab-bar.wasm differ diff --git a/zellij-utils/assets/status-bar.wasm b/zellij-utils/assets/status-bar.wasm old mode 100644 new mode 100755 index 5b4f6eb0..66e6b10d Binary files a/zellij-utils/assets/status-bar.wasm and b/zellij-utils/assets/status-bar.wasm differ diff --git a/zellij-utils/assets/strider.wasm b/zellij-utils/assets/strider.wasm old mode 100644 new mode 100755 index 0af68b2f..287385a5 Binary files a/zellij-utils/assets/strider.wasm and b/zellij-utils/assets/strider.wasm differ diff --git a/zellij-utils/assets/tab-bar.wasm b/zellij-utils/assets/tab-bar.wasm old mode 100644 new mode 100755 index 1166de83..b22bcf10 Binary files a/zellij-utils/assets/tab-bar.wasm and b/zellij-utils/assets/tab-bar.wasm differ diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index aa8a27ea..14bcfae7 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -7,6 +7,7 @@ use crate::{ use clap::{Parser, Subcommand}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use url::Url; #[derive(Parser, Default, Debug, Clone, Serialize, Deserialize)] #[clap(version, name = "zellij")] @@ -373,4 +374,7 @@ pub enum CliAction { NextSwapLayout, /// Query all tab names QueryTabNames, + StartOrReloadPlugin { + url: Url, + }, } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index e23c22bb..8b49112b 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -326,10 +326,13 @@ pub enum ScreenContext { NextSwapLayout, QueryTabNames, NewTiledPluginPane, + StartOrReloadPluginPane, NewFloatingPluginPane, AddPlugin, UpdatePluginLoadingStage, ProgressPluginLoadingOffset, + StartPluginLoadingIndication, + RequestStateUpdateForPlugins, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. @@ -355,6 +358,7 @@ pub enum PluginContext { Update, Render, Unload, + Reload, Resize, Exit, AddClient, @@ -489,6 +493,9 @@ open an issue on GitHub: #[error("Client {client_id} is too slow to handle incoming messages")] ClientTooSlow { client_id: u16 }, + + #[error("The plugin does not exist")] + PluginDoesNotExist, } #[cfg(not(target_family = "wasm"))] diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index d03fd120..c288b8d2 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::str::FromStr; +use url::Url; use crate::position::Position; @@ -231,6 +232,7 @@ pub enum Action { /// Open a new tiled (embedded, non-floating) plugin pane NewTiledPluginPane(RunPluginLocation, Option), // String is an optional name NewFloatingPluginPane(RunPluginLocation, Option), // String is an optional name + StartOrReloadPlugin(Url), } impl Action { @@ -471,6 +473,7 @@ impl Action { CliAction::PreviousSwapLayout => Ok(vec![Action::PreviousSwapLayout]), CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]), CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]), + CliAction::StartOrReloadPlugin { url } => Ok(vec![Action::StartOrReloadPlugin(url)]), } } } diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 830f7c02..713b2775 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -207,7 +207,7 @@ impl Run { } } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] pub struct RunPlugin { #[serde(default)] pub _allow_exec_host_cmd: bool,