From c11d75f9157873fc99fe0d40933de8ec5fbb4f6b Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 7 Jun 2023 12:43:35 +0200 Subject: feat(wasm-plugin-system): major overhaul and some goodies (#2510) * strider resiliency * worker channel prototype * finalized ui * show hide plugin * fs events to plugins * tests for events and new screen instructions * various refactoringz * report plugin errors instead of crashing zellij * fix plugin loading with workers * refactor: move watch filesystem * some fixes and refactoring * refactor(panes): combine pane insertion logic * refactor(screen): launch or focus * refactor(pty): consolidate default shell fetching * refactor: various cleanups * initial refactoring * more initial refactoring * refactor(strider): search * style(fmt): rustfmt * style(pty): cleanup * style(clippy): ok clippy * style(fmt): rustfmt --- zellij-server/src/lib.rs | 9 +- zellij-server/src/panes/floating_panes/mod.rs | 17 +- zellij-server/src/panes/plugin_pane.rs | 2 +- zellij-server/src/panes/tiled_panes/mod.rs | 20 +- zellij-server/src/plugins/mod.rs | 13 +- zellij-server/src/plugins/plugin_loader.rs | 43 +- zellij-server/src/plugins/plugin_map.rs | 70 +--- zellij-server/src/plugins/plugin_worker.rs | 89 ++++ zellij-server/src/plugins/unit/plugin_tests.rs | 82 +++- ...__plugin_tests__can_subscribe_to_hd_events.snap | 12 + zellij-server/src/plugins/wasm_bridge.rs | 150 +++---- zellij-server/src/plugins/watch_filesystem.rs | 63 +++ zellij-server/src/plugins/zellij_exports.rs | 123 +++++- zellij-server/src/pty.rs | 14 +- zellij-server/src/route.rs | 10 + zellij-server/src/screen.rs | 77 +++- zellij-server/src/tab/mod.rs | 399 ++++++++---------- .../src/tab/unit/tab_integration_tests.rs | 457 +++++++++++---------- zellij-server/src/tab/unit/tab_tests.rs | 26 +- zellij-server/src/ui/loading_indication.rs | 16 + zellij-server/src/unit/screen_tests.rs | 156 ++++++- ...__screen_tests__screen_can_suppress_pane-2.snap | 26 ++ ...__screen_tests__screen_can_suppress_pane-3.snap | 6 + ...en__screen_tests__screen_can_suppress_pane.snap | 26 ++ ...ts__send_cli_launch_or_focus_plugin_action.snap | 25 ++ 25 files changed, 1297 insertions(+), 634 deletions(-) create mode 100644 zellij-server/src/plugins/plugin_worker.rs create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__can_subscribe_to_hd_events.snap create mode 100644 zellij-server/src/plugins/watch_filesystem.rs create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane-2.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane-3.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_suppress_pane.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap (limited to 'zellij-server') diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 166a60c5f..4e4911429 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -32,7 +32,7 @@ use wasmer::Store; use crate::{ os_input_output::ServerOsApi, plugins::{plugin_thread_main, PluginInstruction}, - pty::{pty_thread_main, Pty, PtyInstruction}, + pty::{get_default_shell, pty_thread_main, Pty, PtyInstruction}, screen::{screen_thread_main, ScreenInstruction}, thread_bus::{Bus, ThreadSenders}, }; @@ -705,6 +705,10 @@ fn init_session( ..Default::default() }) }); + let path_to_default_shell = config_options + .default_shell + .clone() + .unwrap_or_else(|| get_default_shell()); let pty_thread = thread::Builder::new() .name("pty".to_string()) @@ -757,6 +761,7 @@ fn init_session( }) .unwrap(); + let zellij_cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); let plugin_thread = thread::Builder::new() .name("wasm".to_string()) .spawn({ @@ -780,6 +785,8 @@ fn init_session( data_dir, plugins.unwrap_or_default(), layout, + path_to_default_shell, + zellij_cwd, ) .fatal() } diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index a85cf80fd..071f6e7c2 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -25,7 +25,7 @@ use zellij_utils::{ data::{ModeInfo, Style}, errors::prelude::*, input::command::RunCommand, - input::layout::FloatingPaneLayout, + input::layout::{FloatingPaneLayout, Run, RunPlugin}, pane_size::{Dimension, Offset, PaneGeom, Size, SizeInPixels, Viewport}, }; @@ -870,4 +870,19 @@ impl FloatingPanes { self.focus_pane_for_all_clients(active_pane_id); } } + pub fn get_plugin_pane_id(&self, run_plugin: &RunPlugin) -> Option { + let run = Some(Run::Plugin(run_plugin.clone())); + self.panes + .iter() + .find(|(_id, s_p)| s_p.invoked_with() == &run) + .map(|(id, _)| *id) + } + pub fn focus_pane_if_exists(&mut self, pane_id: PaneId, client_id: ClientId) -> Result<()> { + if self.panes.get(&pane_id).is_some() { + self.focus_pane(pane_id, client_id); + Ok(()) + } else { + Err(anyhow!("Pane not found")) + } + } } diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 7915069c5..28fdb3d10 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -537,7 +537,7 @@ impl Pane for PluginPane { self.pane_title = title; } fn update_loading_indication(&mut self, loading_indication: LoadingIndication) { - if self.loading_indication.ended { + if self.loading_indication.ended && !loading_indication.is_error() { return; } self.loading_indication.merge(loading_indication); diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index 08a633b0a..3734214af 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -20,7 +20,10 @@ use stacked_panes::StackedPanes; use zellij_utils::{ data::{Direction, ModeInfo, ResizeStrategy, Style}, errors::prelude::*, - input::{command::RunCommand, layout::SplitDirection}, + input::{ + command::RunCommand, + layout::{Run, RunPlugin, SplitDirection}, + }, pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport}, }; @@ -529,6 +532,14 @@ impl TiledPanes { } self.reset_boundaries(); } + pub fn focus_pane_if_exists(&mut self, pane_id: PaneId, client_id: ClientId) -> Result<()> { + if self.panes.get(&pane_id).is_some() { + self.focus_pane(pane_id, client_id); + Ok(()) + } else { + Err(anyhow!("Pane not found")) + } + } pub fn focus_pane_at_position(&mut self, position_and_size: PaneGeom, client_id: ClientId) { if let Some(pane_id) = self .panes @@ -1691,6 +1702,13 @@ impl TiledPanes { fn reset_boundaries(&mut self) { self.client_id_to_boundaries.clear(); } + pub fn get_plugin_pane_id(&self, run_plugin: &RunPlugin) -> Option { + let run = Some(Run::Plugin(run_plugin.clone())); + self.panes + .iter() + .find(|(_id, s_p)| s_p.invoked_with() == &run) + .map(|(id, _)| *id) + } } #[allow(clippy::borrowed_box)] diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 9384b3f62..7b322b054 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -1,6 +1,8 @@ mod plugin_loader; mod plugin_map; +mod plugin_worker; mod wasm_bridge; +mod watch_filesystem; mod zellij_exports; use log::info; use std::{collections::HashMap, fs, path::PathBuf}; @@ -104,13 +106,22 @@ pub(crate) fn plugin_thread_main( data_dir: PathBuf, plugins: PluginsConfig, layout: Box, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result<()> { info!("Wasm main thread starts"); let plugin_dir = data_dir.join("plugins/"); let plugin_global_data_dir = plugin_dir.join("data"); - let mut wasm_bridge = WasmBridge::new(plugins, bus.senders.clone(), store, plugin_dir); + let mut wasm_bridge = WasmBridge::new( + plugins, + bus.senders.clone(), + store, + plugin_dir, + path_to_default_shell, + zellij_cwd, + ); loop { let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs index 91eb6c93e..4d77bd9e9 100644 --- a/zellij-server/src/plugins/plugin_loader.rs +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -1,6 +1,5 @@ -use crate::plugins::plugin_map::{ - PluginEnv, PluginMap, RunningPlugin, RunningWorker, Subscriptions, -}; +use crate::plugins::plugin_map::{PluginEnv, PluginMap, RunningPlugin, Subscriptions}; +use crate::plugins::plugin_worker::{plugin_worker, RunningWorker}; use crate::plugins::zellij_exports::{wasi_read_string, zellij_exports}; use crate::plugins::PluginId; use highway::{HighwayHash, PortableHash}; @@ -164,6 +163,8 @@ pub struct PluginLoader<'a> { plugin_own_data_dir: PathBuf, size: Size, wasm_blob_on_hd: Option<(Vec, PathBuf)>, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, } impl<'a> PluginLoader<'a> { @@ -176,6 +177,8 @@ impl<'a> PluginLoader<'a> { plugin_map: Arc>, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result<()> { let err_context = || format!("failed to reload plugin {plugin_id} from memory"); let mut connected_clients: Vec = @@ -194,6 +197,8 @@ impl<'a> PluginLoader<'a> { first_client_id, &store, &plugin_dir, + path_to_default_shell, + zellij_cwd, )?; plugin_loader .load_module_from_memory() @@ -227,6 +232,8 @@ impl<'a> PluginLoader<'a> { size: Size, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result<()> { let err_context = || format!("failed to start plugin {plugin_id} for client {client_id}"); let mut plugin_loader = PluginLoader::new( @@ -240,6 +247,8 @@ impl<'a> PluginLoader<'a> { &plugin_dir, tab_index, size, + path_to_default_shell, + zellij_cwd, )?; plugin_loader .load_module_from_memory() @@ -273,6 +282,8 @@ impl<'a> PluginLoader<'a> { plugin_map: Arc>, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result<()> { let mut new_plugins = HashSet::new(); for plugin_id in plugin_map.lock().unwrap().plugin_ids() { @@ -288,6 +299,8 @@ impl<'a> PluginLoader<'a> { existing_client_id, &store, &plugin_dir, + path_to_default_shell.clone(), + zellij_cwd.clone(), )?; plugin_loader .load_module_from_memory() @@ -314,6 +327,8 @@ impl<'a> PluginLoader<'a> { plugin_map: Arc>, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result<()> { let err_context = || format!("failed to reload plugin id {plugin_id}"); @@ -333,6 +348,8 @@ impl<'a> PluginLoader<'a> { first_client_id, &store, &plugin_dir, + path_to_default_shell, + zellij_cwd, )?; plugin_loader .compile_module() @@ -363,6 +380,8 @@ impl<'a> PluginLoader<'a> { plugin_dir: &'a PathBuf, tab_index: usize, size: Size, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result { let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR .join(Url::from(&plugin.location).to_string()) @@ -383,6 +402,8 @@ impl<'a> PluginLoader<'a> { plugin_own_data_dir, size, wasm_blob_on_hd: None, + path_to_default_shell, + zellij_cwd, }) } pub fn new_from_existing_plugin_attributes( @@ -394,6 +415,8 @@ impl<'a> PluginLoader<'a> { client_id: ClientId, store: &Store, plugin_dir: &'a PathBuf, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result { let err_context = || "Failed to find existing plugin"; let (running_plugin, _subscriptions, _workers) = { @@ -421,6 +444,8 @@ impl<'a> PluginLoader<'a> { plugin_dir, tab_index, size, + path_to_default_shell, + zellij_cwd, ) } pub fn new_from_different_client_id( @@ -432,6 +457,8 @@ impl<'a> PluginLoader<'a> { client_id: ClientId, store: &Store, plugin_dir: &'a PathBuf, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Result { let err_context = || "Failed to find existing plugin"; let running_plugin = { @@ -460,6 +487,8 @@ impl<'a> PluginLoader<'a> { plugin_dir, tab_index, size, + path_to_default_shell, + zellij_cwd, ) } pub fn load_module_from_memory(&mut self) -> Result { @@ -625,7 +654,8 @@ impl<'a> PluginLoader<'a> { let worker = RunningWorker::new(instance, &function_name, plugin_config, plugin_env); - workers.insert(function_name.into(), Arc::new(Mutex::new(worker))); + let worker_sender = plugin_worker(worker); + workers.insert(function_name.into(), worker_sender); } } start_function.call(&[]).with_context(err_context)?; @@ -689,6 +719,8 @@ impl<'a> PluginLoader<'a> { *client_id, &self.store, &self.plugin_dir, + self.path_to_default_shell.clone(), + self.zellij_cwd.clone(), )?; plugin_loader_for_client .load_module_from_memory() @@ -746,7 +778,7 @@ impl<'a> PluginLoader<'a> { }; let mut wasi_env = WasiState::new("Zellij") .env("CLICOLOR_FORCE", "1") - .map_dir("/host", ".") + .map_dir("/host", self.zellij_cwd.clone()) .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| { @@ -771,6 +803,7 @@ impl<'a> PluginLoader<'a> { wasi_env, plugin_own_data_dir: self.plugin_own_data_dir.clone(), tab_index: self.tab_index, + path_to_default_shell: self.path_to_default_shell.clone(), }; let subscriptions = Arc::new(Mutex::new(HashSet::new())); diff --git a/zellij-server/src/plugins/plugin_map.rs b/zellij-server/src/plugins/plugin_map.rs index 0c3df931e..040c9f9fd 100644 --- a/zellij-server/src/plugins/plugin_map.rs +++ b/zellij-server/src/plugins/plugin_map.rs @@ -1,5 +1,4 @@ -use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError}; -use crate::plugins::zellij_exports::wasi_write_object; +use crate::plugins::plugin_worker::MessageToWorker; use crate::plugins::PluginId; use std::{ collections::{HashMap, HashSet}, @@ -11,10 +10,10 @@ use wasmer_wasi::WasiEnv; use crate::{thread_bus::ThreadSenders, ClientId}; +use zellij_utils::async_channel::Sender; use zellij_utils::errors::prelude::*; use zellij_utils::{ - consts::VERSION, data::EventType, input::layout::RunPluginLocation, - input::plugins::PluginConfig, + data::EventType, input::layout::RunPluginLocation, input::plugins::PluginConfig, }; // the idea here is to provide atomicity when adding/removing plugins from the map (eg. when a new @@ -29,7 +28,7 @@ pub struct PluginMap { ( Arc>, Arc>, - HashMap>>, + HashMap>, ), >, } @@ -41,7 +40,7 @@ impl PluginMap { ) -> Vec<( Arc>, Arc>, - HashMap>>, + HashMap>, )> { let mut removed = vec![]; let ids_in_plugin_map: Vec<(PluginId, ClientId)> = @@ -62,7 +61,7 @@ impl PluginMap { ) -> Option<( Arc>, Arc>, - HashMap>>, + HashMap>, )> { self.plugin_assets.remove(&(plugin_id, client_id)) } @@ -132,12 +131,12 @@ impl PluginMap { .and_then(|(_, (running_plugin, _, _))| Some(running_plugin.clone())), } } - pub fn clone_worker( + pub fn worker_sender( &self, plugin_id: PluginId, client_id: ClientId, worker_name: &str, - ) -> Option>> { + ) -> Option> { self.plugin_assets .iter() .find(|((p_id, c_id), _)| p_id == &plugin_id && c_id == &client_id) @@ -174,7 +173,7 @@ impl PluginMap { client_id: ClientId, running_plugin: Arc>, subscriptions: Arc>, - running_workers: HashMap>>, + running_workers: HashMap>, ) { self.plugin_assets.insert( (plugin_id, client_id), @@ -195,6 +194,7 @@ pub struct PluginEnv { pub client_id: ClientId, #[allow(dead_code)] pub plugin_own_data_dir: PathBuf, + pub path_to_default_shell: PathBuf, } impl PluginEnv { @@ -256,53 +256,3 @@ impl RunningPlugin { } } } - -pub struct RunningWorker { - pub instance: Instance, - pub name: String, - pub plugin_config: PluginConfig, - pub plugin_env: PluginEnv, -} - -impl RunningWorker { - pub fn new( - instance: Instance, - name: &str, - plugin_config: PluginConfig, - plugin_env: PluginEnv, - ) -> Self { - RunningWorker { - instance, - name: name.into(), - plugin_config, - plugin_env, - } - } - pub fn send_message(&self, message: String, payload: String) -> Result<()> { - let err_context = || format!("Failed to send message to worker"); - - let work_function = self - .instance - .exports - .get_function(&self.name) - .with_context(err_context)?; - wasi_write_object(&self.plugin_env.wasi_env, &(message, payload)) - .with_context(err_context)?; - work_function.call(&[]).or_else::(|e| { - match e.downcast::() { - Ok(_) => panic!( - "{}", - anyError::new(VersionMismatchError::new( - VERSION, - "Unavailable", - &self.plugin_config.path, - self.plugin_config.is_builtin(), - )) - ), - Err(e) => Err(e).with_context(err_context), - } - })?; - - Ok(()) - } -} diff --git a/zellij-server/src/plugins/plugin_worker.rs b/zellij-server/src/plugins/plugin_worker.rs new file mode 100644 index 000000000..bc7303c7c --- /dev/null +++ b/zellij-server/src/plugins/plugin_worker.rs @@ -0,0 +1,89 @@ +use crate::plugins::plugin_loader::VersionMismatchError; +use crate::plugins::plugin_map::PluginEnv; +use crate::plugins::zellij_exports::wasi_write_object; +use wasmer::Instance; + +use zellij_utils::async_channel::{unbounded, Receiver, Sender}; +use zellij_utils::async_std::task; +use zellij_utils::errors::prelude::*; +use zellij_utils::{consts::VERSION, input::plugins::PluginConfig}; + +pub struct RunningWorker { + pub instance: Instance, + pub name: String, + pub plugin_config: PluginConfig, + pub plugin_env: PluginEnv, +} + +impl RunningWorker { + pub fn new( + instance: Instance, + name: &str, + plugin_config: PluginConfig, + plugin_env: PluginEnv, + ) -> Self { + RunningWorker { + instance, + name: name.into(), + plugin_config, + plugin_env, + } + } + pub fn send_message(&self, message: String, payload: String) -> Result<()> { + let err_context = || format!("Failed to send message to worker"); + + let work_function = self + .instance + .exports + .get_function(&self.name) + .with_context(err_context)?; + wasi_write_object(&self.plugin_env.wasi_env, &(message, payload)) + .with_context(err_context)?; + work_function.call(&[]).or_else::(|e| { + match e.downcast::() { + Ok(_) => panic!( + "{}", + anyError::new(VersionMismatchError::new( + VERSION, + "Unavailable", + &self.plugin_config.path, + self.plugin_config.is_builtin(), + )) + ), + Err(e) => Err(e).with_context(err_context), + } + })?; + + Ok(()) + } +} + +pub enum MessageToWorker { + Message(String, String), // message, payload + Exit, +} + +pub fn plugin_worker(worker: RunningWorker) -> Sender { + let (sender, receiver): (Sender, Receiver) = unbounded(); + task::spawn({ + async move { + loop { + match receiver.recv().await { + Ok(MessageToWorker::Message(message, payload)) => { + if let Err(e) = worker.send_message(message, payload) { + log::error!("Failed to send message to worker: {:?}", e); + } + }, + Ok(MessageToWorker::Exit) => { + break; + }, + Err(e) => { + log::error!("Failed to receive worker message on channel: {:?}", e); + break; + }, + } + } + } + }); + sender +} diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index d1e79a5fe..d41a477b7 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -50,11 +50,14 @@ macro_rules! log_actions_in_thread { }; } -fn create_plugin_thread() -> ( +fn create_plugin_thread( + zellij_cwd: Option, +) -> ( SenderWithContext, Receiver<(ScreenInstruction, ErrorContext)>, Box, ) { + let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from(".")); let (to_server, _server_receiver): ChannelWithContext = channels::bounded(50); let to_server = SenderWithContext::new(to_server); @@ -88,6 +91,7 @@ fn create_plugin_thread() -> ( .should_silently_fail(); let store = Store::new(&wasmer::Universal::new(wasmer::Singlepass::default()).engine()); let data_dir = PathBuf::from(tempdir().unwrap().path()); + let default_shell = PathBuf::from("."); let _plugin_thread = std::thread::Builder::new() .name("plugin_thread".to_string()) .spawn(move || { @@ -98,6 +102,8 @@ fn create_plugin_thread() -> ( data_dir, PluginsConfig::default(), Box::new(Layout::default()), + default_shell, + zellij_cwd, ) .expect("TEST") }) @@ -134,7 +140,7 @@ pub fn load_new_plugin_from_hd() { // message (this is what the fixture plugin does) // we then listen on our mock screen receiver to make sure we got a PluginBytes instruction // that contains said render, and assert against it - let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(); + let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); let run_plugin = RunPlugin { @@ -192,7 +198,7 @@ pub fn load_new_plugin_from_hd() { #[test] #[ignore] pub fn plugin_workers() { - let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(); + let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); let run_plugin = RunPlugin { @@ -253,7 +259,7 @@ pub fn plugin_workers() { #[test] #[ignore] pub fn plugin_workers_persist_state() { - let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(); + let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None); let plugin_should_float = Some(false); let plugin_title = Some("test_plugin".to_owned()); let run_plugin = RunPlugin { @@ -318,3 +324,71 @@ pub fn plugin_workers_persist_state() { }); assert_snapshot!(format!("{:#?}", plugin_bytes_event)); } + +#[test] +#[ignore] +pub fn can_subscribe_to_hd_events() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::PluginBytes, + screen_receiver, + 3 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::InputReceived, + )])); // will be cached and sent to the plugin once it's loaded + std::thread::sleep(std::time::Duration::from_millis(100)); + std::fs::OpenOptions::new() + .create(true) + .write(true) + .open(PathBuf::from(temp_folder.path()).join("test1")) + .unwrap(); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let plugin_bytes_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::PluginBytes(plugin_bytes) = i { + for (plugin_id, client_id, plugin_bytes) in plugin_bytes { + let plugin_bytes = String::from_utf8_lossy(plugin_bytes).to_string(); + if plugin_bytes.contains("FileSystem") { + return Some((*plugin_id, *client_id, plugin_bytes)); + } + } + } + None + }); + assert_snapshot!(format!("{:#?}", plugin_bytes_event)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__can_subscribe_to_hd_events.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__can_subscribe_to_hd_events.snap new file mode 100644 index 000000000..57e31f15e --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__can_subscribe_to_hd_events.snap @@ -0,0 +1,12 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 387 +expression: "format!(\"{:#?}\", plugin_bytes_event)" +--- +Some( + ( + 0, + 1, + "Rows: 20, Cols: 121, Received events: [InputReceived, FileSystemRead([\"/host/test1\"])]\n\r", + ), +) diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index 0015552c1..1ede55e23 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -1,24 +1,24 @@ use super::{PluginId, PluginInstruction}; use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError}; -use crate::plugins::plugin_map::{ - AtomicEvent, PluginEnv, PluginMap, RunningPlugin, RunningWorker, Subscriptions, -}; +use crate::plugins::plugin_map::{AtomicEvent, PluginEnv, PluginMap, RunningPlugin, Subscriptions}; +use crate::plugins::plugin_worker::MessageToWorker; +use crate::plugins::watch_filesystem::watch_filesystem; use crate::plugins::zellij_exports::{wasi_read_string, wasi_write_object}; use log::info; use std::{ collections::{HashMap, HashSet}, path::PathBuf, str::FromStr, - sync::{Arc, Mutex, TryLockError}, + sync::{Arc, Mutex}, }; use wasmer::{Instance, Module, Store, Value}; use zellij_utils::async_std::task::{self, JoinHandle}; +use zellij_utils::notify::{RecommendedWatcher, Watcher}; use crate::{ background_jobs::BackgroundJob, screen::ScreenInstruction, thread_bus::ThreadSenders, ui::loading_indication::LoadingIndication, ClientId, }; - use zellij_utils::{ consts::VERSION, data::{Event, EventType}, @@ -30,8 +30,6 @@ use zellij_utils::{ pane_size::Size, }; -const RETRY_INTERVAL_MS: u64 = 100; - pub struct WasmBridge { connected_clients: Arc>>, plugins: PluginsConfig, @@ -49,6 +47,9 @@ pub struct WasmBridge { // payload> loading_plugins: HashMap<(PluginId, RunPlugin), JoinHandle<()>>, // plugin_id to join-handle pending_plugin_reloads: HashSet, + path_to_default_shell: PathBuf, + watcher: Option, + zellij_cwd: PathBuf, } impl WasmBridge { @@ -57,11 +58,20 @@ impl WasmBridge { senders: ThreadSenders, store: Store, plugin_dir: PathBuf, + path_to_default_shell: PathBuf, + zellij_cwd: PathBuf, ) -> Self { let plugin_map = Arc::new(Mutex::new(PluginMap::default())); let connected_clients: Arc>> = Arc::new(Mutex::new(vec![])); let plugin_cache: Arc>> = Arc::new(Mutex::new(HashMap::new())); + let watcher = match watch_filesystem(senders.clone(), &zellij_cwd) { + Ok(watcher) => Some(watcher), + Err(e) => { + log::error!("Failed to watch filesystem: {:?}", e); + None + }, + }; WasmBridge { connected_clients, plugins, @@ -70,12 +80,15 @@ impl WasmBridge { plugin_dir, plugin_cache, plugin_map, + path_to_default_shell, + watcher, next_plugin_id: 0, cached_events_for_pending_plugins: HashMap::new(), cached_resizes_for_pending_plugins: HashMap::new(), cached_worker_messages: HashMap::new(), loading_plugins: HashMap::new(), pending_plugin_reloads: HashSet::new(), + zellij_cwd, } } pub fn load_plugin( @@ -122,6 +135,8 @@ impl WasmBridge { let store = self.store.clone(); let plugin_map = self.plugin_map.clone(); let connected_clients = self.connected_clients.clone(); + let path_to_default_shell = self.path_to_default_shell.clone(); + let zellij_cwd = self.zellij_cwd.clone(); async move { let _ = senders.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id)); @@ -139,6 +154,8 @@ impl WasmBridge { size, connected_clients.clone(), &mut loading_indication, + path_to_default_shell, + zellij_cwd.clone(), ) { Ok(_) => handle_plugin_successful_loading(&senders, plugin_id), Err(e) => handle_plugin_loading_failure( @@ -160,7 +177,10 @@ impl WasmBridge { pub fn unload_plugin(&mut self, pid: PluginId) -> Result<()> { info!("Bye from plugin {}", &pid); let mut plugin_map = self.plugin_map.lock().unwrap(); - for (running_plugin, _, _) in plugin_map.remove_plugins(pid) { + for (running_plugin, _, workers) in plugin_map.remove_plugins(pid) { + for (_worker_name, worker_sender) in workers { + drop(worker_sender.send(MessageToWorker::Exit)); + } let running_plugin = running_plugin.lock().unwrap(); let cache_dir = running_plugin.plugin_env.plugin_own_data_dir.clone(); if let Err(e) = std::fs::remove_dir_all(cache_dir) { @@ -195,6 +215,8 @@ impl WasmBridge { let store = self.store.clone(); let plugin_map = self.plugin_map.clone(); let connected_clients = self.connected_clients.clone(); + let path_to_default_shell = self.path_to_default_shell.clone(); + let zellij_cwd = self.zellij_cwd.clone(); async move { match PluginLoader::reload_plugin( first_plugin_id, @@ -205,6 +227,8 @@ impl WasmBridge { plugin_map.clone(), connected_clients.clone(), &mut loading_indication, + path_to_default_shell.clone(), + zellij_cwd.clone(), ) { Ok(_) => { handle_plugin_successful_loading(&senders, first_plugin_id); @@ -223,6 +247,8 @@ impl WasmBridge { plugin_map.clone(), connected_clients.clone(), &mut loading_indication, + path_to_default_shell.clone(), + zellij_cwd.clone(), ) { Ok(_) => handle_plugin_successful_loading(&senders, *plugin_id), Err(e) => handle_plugin_loading_failure( @@ -263,6 +289,8 @@ impl WasmBridge { self.plugin_map.clone(), self.connected_clients.clone(), &mut loading_indication, + self.path_to_default_shell.clone(), + self.zellij_cwd.clone(), ) { Ok(_) => { let _ = self @@ -414,7 +442,17 @@ impl WasmBridge { )); }, Err(e) => { - log::error!("{}", e); + log::error!("{:?}", e); + + // https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c + let stringified_error = + format!("{:?}", e).replace("\n", "\n\r"); + + handle_plugin_crash( + plugin_id, + stringified_error, + senders.clone(), + ); }, } } @@ -460,6 +498,7 @@ impl WasmBridge { for plugin_id in &plugin_ids { drop(self.unload_plugin(*plugin_id)); } + drop(self.watcher.as_mut().map(|w| w.unwatch(&self.zellij_cwd))); } fn run_plugin_of_plugin_id(&self, plugin_id: PluginId) -> Option<&RunPlugin> { self.loading_plugins @@ -599,36 +638,23 @@ impl WasmBridge { self.plugin_map .lock() .unwrap() - .clone_worker(plugin_id, client_id, &worker_name); - let mut cache_messages = || { - for (message, payload) in messages.drain(..) { - self.cached_worker_messages - .entry(plugin_id) - .or_default() - .push((client_id, worker_name.clone(), message, payload)); - } - }; + .worker_sender(plugin_id, client_id, &worker_name); match worker { Some(worker) => { - let worker_is_busy = { worker.try_lock().is_err() }; - if worker_is_busy { - // most messages will be caught here, we do this once before the async task to - // bulk most messages together and prevent them from cascading - cache_messages(); - } else { - async_send_messages_to_worker( - self.senders.clone(), - messages, - worker, - plugin_id, - client_id, - worker_name, - ); + for (message, payload) in messages.drain(..) { + if let Err(e) = worker.try_send(MessageToWorker::Message(message, payload)) { + log::error!("Failed to send message to worker: {:?}", e); + } } }, None => { - log::warn!("Worker {worker_name} not found, placing message in cache"); - cache_messages(); + log::warn!("Worker {worker_name} not found, caching messages"); + for (message, payload) in messages.drain(..) { + self.cached_worker_messages + .entry(plugin_id) + .or_default() + .push((client_id, worker_name.clone(), message, payload)); + } }, } Ok(()) @@ -708,52 +734,12 @@ pub fn apply_event_to_plugin( Ok(()) } -fn async_send_messages_to_worker( - senders: ThreadSenders, - mut messages: Vec<(String, String)>, - worker: Arc>, - plugin_id: PluginId, - client_id: ClientId, - worker_name: String, -) { - task::spawn({ - async move { - match worker.try_lock() { - Ok(worker) => { - for (message, payload) in messages.drain(..) { - worker.send_message(message, payload).ok(); - } - let _ = senders - .send_to_plugin(PluginInstruction::ApplyCachedWorkerMessages(plugin_id)); - }, - Err(TryLockError::WouldBlock) => { - task::spawn({ - async move { - log::warn!( - "Worker {} busy, retrying sending message after: {}ms", - worker_name, - RETRY_INTERVAL_MS - ); - task::sleep(std::time::Duration::from_millis(RETRY_INTERVAL_MS)).await; - let _ = senders.send_to_plugin( - PluginInstruction::PostMessagesToPluginWorker( - plugin_id, - client_id, - worker_name, - messages, - ), - ); - } - }); - }, - Err(e) => { - log::error!( - "Failed to send message to worker \"{}\": {:?}", - worker_name, - e - ); - }, - } - } - }); +pub fn handle_plugin_crash(plugin_id: PluginId, message: String, senders: ThreadSenders) { + let mut loading_indication = LoadingIndication::new("Panic!".to_owned()); + loading_indication.indicate_loading_error(message); + let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage( + plugin_id, + loading_indication, + )); + let _ = senders.send_to_plugin(PluginInstruction::Unload(plugin_id)); } diff --git a/zellij-server/src/plugins/watch_filesystem.rs b/zellij-server/src/plugins/watch_filesystem.rs new file mode 100644 index 000000000..864bd45f9 --- /dev/null +++ b/zellij-server/src/plugins/watch_filesystem.rs @@ -0,0 +1,63 @@ +use super::PluginInstruction; +use std::path::PathBuf; + +use crate::thread_bus::ThreadSenders; +use std::path::Path; + +use zellij_utils::{data::Event, errors::prelude::*}; + +use zellij_utils::notify::{self, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +pub fn watch_filesystem(senders: ThreadSenders, zellij_cwd: &Path) -> Result { + let path_prefix_in_plugins = PathBuf::from("/host"); + let current_dir = PathBuf::from(zellij_cwd); + let mut watcher = notify::recommended_watcher({ + move |res: notify::Result| match res { + Ok(event) => { + let paths: Vec = event + .paths + .iter() + .map(|p| { + let stripped_prefix_path = + p.strip_prefix(¤t_dir).unwrap_or_else(|_| p); + path_prefix_in_plugins.join(stripped_prefix_path) + }) + .collect(); + match event.kind { + EventKind::Access(_) => { + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::FileSystemRead(paths), + )])); + }, + EventKind::Create(_) => { + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::FileSystemCreate(paths), + )])); + }, + EventKind::Modify(_) => { + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::FileSystemUpdate(paths), + )])); + }, + EventKind::Remove(_) => { + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::FileSystemDelete(paths), + )])); + }, + _ => {}, + } + }, + Err(e) => log::error!("watch error: {:?}", e), + } + })?; + + watcher.watch(zellij_cwd, RecursiveMode::Recursive)?; + Ok(watcher) +} diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 3515cb348..d64d867b7 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -1,5 +1,6 @@ use super::PluginInstruction; use crate::plugins::plugin_map::{PluginEnv, Subscriptions}; +use crate::plugins::wasm_bridge::handle_plugin_crash; use log::{debug, warn}; use serde::{de::DeserializeOwned, Serialize}; use std::{ @@ -23,7 +24,10 @@ use zellij_utils::{ consts::VERSION, data::{Event, EventType, PluginIds}, errors::prelude::*, - input::{command::TerminalAction, plugins::PluginType}, + input::{ + command::{RunCommand, TerminalAction}, + plugins::PluginType, + }, serde, }; @@ -50,13 +54,18 @@ pub fn zellij_exports( host_get_plugin_ids, host_get_zellij_version, host_open_file, + host_open_file_floating, host_open_file_with_line, + host_open_file_with_line_floating, + host_open_terminal, + host_open_terminal_floating, host_switch_tab_to, host_set_timeout, host_exec_cmd, host_report_panic, host_post_message_to, host_post_message_to_plugin, + host_hide_self, } } @@ -160,9 +169,30 @@ fn host_open_file(env: &ForeignFunctionEnv) { .senders .send_to_pty(PtyInstruction::SpawnTerminal( Some(TerminalAction::OpenFile(path, None, None)), + Some(false), None, + ClientOrTabIndex::ClientId(env.plugin_env.client_id), + )) + }) + .with_context(|| { + format!( + "failed to open file on host from plugin {}", + env.plugin_env.name() + ) + }) + .non_fatal(); +} + +fn host_open_file_floating(env: &ForeignFunctionEnv) { + wasi_read_object::(&env.plugin_env.wasi_env) + .and_then(|path| { + env.plugin_env + .senders + .send_to_pty(PtyInstruction::SpawnTerminal( + Some(TerminalAction::OpenFile(path, None, None)), + Some(true), None, - ClientOrTabIndex::TabIndex(env.plugin_env.tab_index), + ClientOrTabIndex::ClientId(env.plugin_env.client_id), )) }) .with_context(|| { @@ -181,9 +211,30 @@ fn host_open_file_with_line(env: &ForeignFunctionEnv) { .senders .send_to_pty(PtyInstruction::SpawnTerminal( Some(TerminalAction::OpenFile(path, Some(line), None)), // TODO: add cwd + Some(false), None, + ClientOrTabIndex::ClientId(env.plugin_env.client_id), + )) + }) + .with_context(|| { + format!( + "failed to open file on host from plugin {}", + env.plugin_env.name() + ) + }) + .non_fatal(); +} + +fn host_open_file_with_line_floating(env: &ForeignFunctionEnv) { + wasi_read_object::<(PathBuf, usize)>(&env.plugin_env.wasi_env) + .and_then(|(path, line)| { + env.plugin_env + .senders + .send_to_pty(PtyInstruction::SpawnTerminal( + Some(TerminalAction::OpenFile(path, Some(line), None)), // TODO: add cwd + Some(true), None, - ClientOrTabIndex::TabIndex(env.plugin_env.tab_index), + ClientOrTabIndex::ClientId(env.plugin_env.client_id), )) }) .with_context(|| { @@ -195,6 +246,54 @@ fn host_open_file_with_line(env: &ForeignFunctionEnv) { .non_fatal(); } +fn host_open_terminal(env: &ForeignFunctionEnv) { + wasi_read_object::(&env.plugin_env.wasi_env) + .and_then(|path| { + env.plugin_env + .senders + .send_to_pty(PtyInstruction::SpawnTerminal( + Some(TerminalAction::RunCommand( + RunCommand::new(env.plugin_env.path_to_default_shell.clone()) + .with_cwd(path), + )), + Some(false), + None, + ClientOrTabIndex::ClientId(env.plugin_env.client_id), + )) + }) + .with_context(|| { + format!( + "failed to open terminal on host from plugin {}", + env.plugin_env.name() + ) + }) + .non_fatal(); +} + +fn host_open_terminal_floating(env: &ForeignFunctionEnv) { + wasi_read_object::(&env.plugin_env.wasi_env) + .and_then(|path| { + env.plugin_env + .senders + .send_to_pty(PtyInstruction::SpawnTerminal( + Some(TerminalAction::RunCommand( + RunCommand::new(env.plugin_env.path_to_default_shell.clone()) + .with_cwd(path), + )), + Some(true), + None, + ClientOrTabIndex::ClientId(env.plugin_env.client_id), + )) + }) + .with_context(|| { + format!( + "failed to open terminal on host from plugin {}", + env.plugin_env.name() + ) + }) + .non_fatal(); +} + fn host_switch_tab_to(env: &ForeignFunctionEnv, tab_idx: u32) { env.plugin_env .senders @@ -314,6 +413,17 @@ fn host_post_message_to_plugin(env: &ForeignFunctionEnv) { .fatal(); } +fn host_hide_self(env: &ForeignFunctionEnv) { + env.plugin_env + .senders + .send_to_screen(ScreenInstruction::SuppressPane( + PaneId::Plugin(env.plugin_env.plugin_id), + env.plugin_env.client_id, + )) + .with_context(|| format!("failed to hide self")) + .fatal(); +} + // Custom panic handler for plugins. // // This is called when a panic occurs in a plugin. Since most panics will likely originate in the @@ -328,7 +438,12 @@ fn host_report_panic(env: &ForeignFunctionEnv) { ) }) .fatal(); - panic!("{}", msg); + log::error!("PANIC IN PLUGIN! {}", msg); + handle_plugin_crash( + env.plugin_env.plugin_id, + msg, + env.plugin_env.senders.clone(), + ); } // Helper Functions --------------------------------------------------------------------------------------------------- diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index fb58aef8b..84f282f8d 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -7,7 +7,7 @@ use crate::{ ClientId, ServerInstruction, }; use async_std::task::{self, JoinHandle}; -use std::{collections::HashMap, env, os::unix::io::RawFd, path::PathBuf}; +use std::{collections::HashMap, os::unix::io::RawFd, path::PathBuf}; use zellij_utils::nix::unistd::Pid; use zellij_utils::{ async_std, @@ -468,10 +468,7 @@ impl Pty { default_shell }, None => { - let shell = PathBuf::from(env::var("SHELL").unwrap_or_else(|_| { - log::warn!("Cannot read SHELL env, falling back to use /bin/sh"); - "/bin/sh".to_string() - })); + let shell = get_default_shell(); TerminalAction::RunCommand(RunCommand { args: vec![], command: shell, @@ -1048,3 +1045,10 @@ fn send_command_not_found_to_screen( .with_context(err_context)?; Ok(()) } + +pub fn get_default_shell() -> PathBuf { + PathBuf::from(std::env::var("SHELL").unwrap_or_else(|_| { + log::warn!("Cannot read SHELL env, falling back to use /bin/sh"); + "/bin/sh".to_string() + })) +} diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index f34553796..c43e99f17 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -702,6 +702,16 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, + Action::LaunchOrFocusPlugin(run_plugin, should_float) => { + session + .senders + .send_to_screen(ScreenInstruction::LaunchOrFocusPlugin( + run_plugin, + should_float, + client_id, + )) + .with_context(err_context)?; + }, } Ok(should_break) } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 030a63a57..e88baf29e 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -273,6 +273,8 @@ pub enum ScreenInstruction { StartPluginLoadingIndication(u32, LoadingIndication), // u32 - plugin_id ProgressPluginLoadingOffset(u32), // u32 - plugin id RequestStateUpdateForPlugins, + LaunchOrFocusPlugin(RunPlugin, bool, ClientId), // bool is should_float + SuppressPane(PaneId, ClientId), } impl From<&ScreenInstruction> for ScreenContext { @@ -435,6 +437,8 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::RequestStateUpdateForPlugins => { ScreenContext::RequestStateUpdateForPlugins }, + ScreenInstruction::LaunchOrFocusPlugin(..) => ScreenContext::LaunchOrFocusPlugin, + ScreenInstruction::SuppressPane(..) => ScreenContext::SuppressPane, } } } @@ -1462,6 +1466,24 @@ impl Screen { self.render() } + pub fn focus_plugin_pane( + &mut self, + run_plugin: &RunPlugin, + should_float: bool, + client_id: ClientId, + ) -> Result { + // true => found and focused, false => not + let all_tabs = self.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if let Some(plugin_pane_id) = tab.find_plugin(&run_plugin) { + tab.focus_pane_with_id(plugin_pane_id, should_float, client_id) + .context("failed to focus plugin pane")?; + return Ok(true); + } + } + Ok(false) + } + fn unblock_input(&self) -> Result<()> { self.bus .senders @@ -1557,6 +1579,7 @@ pub(crate) fn screen_thread_main( client_id: ClientId| tab .new_pane(pid, initial_pane_title, should_float, + None, Some(client_id)), ?); if let Some(hold_for_command) = hold_for_command { @@ -1575,7 +1598,13 @@ pub(crate) fn screen_thread_main( }, ClientOrTabIndex::TabIndex(tab_index) => { if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { - active_tab.new_pane(pid, initial_pane_title, should_float, None)?; + active_tab.new_pane( + pid, + initial_pane_title, + should_float, + None, + None, + )?; if let Some(hold_for_command) = hold_for_command { let is_first_run = true; active_tab.hold_pane(pid, None, is_first_run, hold_for_command); @@ -2558,11 +2587,11 @@ pub(crate) fn screen_thread_main( pane_title.unwrap_or_else(|| run_plugin_location.location.to_string()); let run_plugin = Run::Plugin(run_plugin_location); if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { - active_tab.new_plugin_pane( + active_tab.new_pane( PaneId::Plugin(plugin_id), - pane_title, + Some(pane_title), should_float, - run_plugin, + Some(run_plugin), None, )?; } else { @@ -2608,6 +2637,46 @@ pub(crate) fn screen_thread_main( screen.update_tabs()?; screen.render()?; }, + ScreenInstruction::LaunchOrFocusPlugin(run_plugin, should_float, client_id) => { + let client_id = if screen.active_tab_indices.contains_key(&client_id) { + Some(client_id) + } else { + screen.get_first_client_id() + }; + let client_id_and_focused_tab = client_id.and_then(|client_id| { + screen + .active_tab_indices + .get(&client_id) + .map(|tab_index| (*tab_index, client_id)) + }); + match client_id_and_focused_tab { + Some((tab_index, client_id)) => { + if screen.focus_plugin_pane(&run_plugin, should_float, client_id)? { + screen.render()?; + } else { + screen.bus.senders.send_to_plugin(PluginInstruction::Load( + Some(should_float), + None, + run_plugin, + tab_index, + client_id, + Size::default(), + ))?; + } + }, + None => log::error!("No connected clients found - cannot load or focus plugin"), + } + }, + ScreenInstruction::SuppressPane(pane_id, client_id) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&pane_id) { + tab.suppress_pane(pane_id, client_id); + drop(screen.render()); + break; + } + } + }, } } Ok(()) diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 4a37a0903..e695117e2 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -47,8 +47,8 @@ use zellij_utils::{ input::{ command::TerminalAction, layout::{ - FloatingPaneLayout, Run, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout, - TiledPaneLayout, + FloatingPaneLayout, Run, RunPlugin, RunPluginLocation, SwapFloatingLayout, + SwapTiledLayout, TiledPaneLayout, }, parse_keys, }, @@ -901,7 +901,6 @@ impl Tab { pub fn toggle_pane_embed_or_floating(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to toggle embedded/floating pane for client {client_id}"); - if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } @@ -914,55 +913,24 @@ impl Tab { "failed to find floating pane (ID: {focused_floating_pane_id:?}) to embed for client {client_id}", )) .with_context(err_context)?; - self.tiled_panes - .insert_pane(focused_floating_pane_id, floating_pane_to_embed); - self.should_clear_display_before_rendering = true; - self.tiled_panes - .focus_pane(focused_floating_pane_id, client_id); self.hide_floating_panes(); - if self.auto_layout && !self.swap_layouts.is_tiled_damaged() { - // only do this if we're already in this layout, otherwise it might be - // confusing and not what the user intends - self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the - // next layout - self.next_swap_layout(Some(client_id), true)?; - } + self.add_tiled_pane( + floating_pane_to_embed, + focused_floating_pane_id, + Some(client_id), + )?; } } } else if let Some(focused_pane_id) = self.tiled_panes.focused_pane_id(client_id) { - if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() { - if self.get_selectable_tiled_panes().count() <= 1 { - // don't close the only pane on screen... - return Ok(()); - } - if let Some(mut embedded_pane_to_float) = - self.close_pane(focused_pane_id, true, Some(client_id)) - { - if !embedded_pane_to_float.borderless() { - // floating panes always have a frame unless they're explicitly borderless - embedded_pane_to_float.set_content_offset(Offset::frame(1)); - } - embedded_pane_to_float.set_geom(new_pane_geom); - resize_pty!( - embedded_pane_to_float, - self.os_api, - self.senders, - self.character_cell_size - ) - .with_context(err_context)?; - embedded_pane_to_float.set_active_at(Instant::now()); - self.floating_panes - .add_pane(focused_pane_id, embedded_pane_to_float); - self.floating_panes.focus_pane(focused_pane_id, client_id); - self.show_floating_panes(); - if self.auto_layout && !self.swap_layouts.is_floating_damaged() { - // only do this if we're already in this layout, otherwise it might be - // confusing and not what the user intends - self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the - // next layout - self.next_swap_layout(Some(client_id), true)?; - } - } + if self.get_selectable_tiled_panes().count() <= 1 { + // don't close the only pane on screen... + return Ok(()); + } + if let Some(embedded_pane_to_float) = +