From 341f9eb8c8771a59b2e4d238ba49ba88c3720d6d Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Mon, 27 Mar 2023 19:19:34 +0200 Subject: feat(plugins): async plugin loading (#2327) * work * refactor(plugins): break down start plugin async function * work * loading messages * nice ui * floating panes and error handling * cleanups and conflicting plugin/direction * find exact pane when relayouting * fix plugin pane titles * kill loading tasks on exit * refactor: move stuff around * style(fmt): rustfmt * various fixes and refactors --- zellij-server/src/background_jobs.rs | 42 +++ zellij-server/src/panes/plugin_pane.rs | 40 ++- zellij-server/src/plugins/mod.rs | 38 ++- zellij-server/src/plugins/start_plugin.rs | 471 ++++++++++++++++++++++++++ zellij-server/src/plugins/wasm_bridge.rs | 510 +++++++++++++---------------- zellij-server/src/route.rs | 16 + zellij-server/src/screen.rs | 112 ++++++- zellij-server/src/tab/layout_applier.rs | 61 +++- zellij-server/src/tab/mod.rs | 146 ++++++++- zellij-server/src/ui/loading_indication.rs | 260 +++++++++++++++ zellij-server/src/ui/mod.rs | 1 + zellij-server/src/unit/screen_tests.rs | 3 + 12 files changed, 1404 insertions(+), 296 deletions(-) create mode 100644 zellij-server/src/plugins/start_plugin.rs create mode 100644 zellij-server/src/ui/loading_indication.rs (limited to 'zellij-server/src') diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs index c612c9d57..8061b4a47 100644 --- a/zellij-server/src/background_jobs.rs +++ b/zellij-server/src/background_jobs.rs @@ -2,6 +2,10 @@ use zellij_utils::async_std::task; use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType}; use std::collections::HashMap; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; use std::time::{Duration, Instant}; use crate::panes::PaneId; @@ -11,6 +15,8 @@ use crate::thread_bus::Bus; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum BackgroundJob { DisplayPaneError(Vec, String), + AnimatePluginLoading(u32), // u32 - plugin_id + StopPluginLoadingAnimation(u32), // u32 - plugin_id Exit, } @@ -18,16 +24,22 @@ impl From<&BackgroundJob> for BackgroundJobContext { fn from(background_job: &BackgroundJob) -> Self { match *background_job { BackgroundJob::DisplayPaneError(..) => BackgroundJobContext::DisplayPaneError, + BackgroundJob::AnimatePluginLoading(..) => BackgroundJobContext::AnimatePluginLoading, + BackgroundJob::StopPluginLoadingAnimation(..) => { + BackgroundJobContext::StopPluginLoadingAnimation + }, BackgroundJob::Exit => BackgroundJobContext::Exit, } } } static FLASH_DURATION_MS: u64 = 1000; +static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500; pub(crate) fn background_jobs_main(bus: Bus) -> Result<()> { let err_context = || "failed to write to pty".to_string(); let mut running_jobs: HashMap = HashMap::new(); + let mut loading_plugins: HashMap> = HashMap::new(); // u32 - plugin_id loop { let (event, mut err_ctx) = bus.recv().with_context(err_context)?; @@ -54,7 +66,37 @@ pub(crate) fn background_jobs_main(bus: Bus) -> Result<()> { } }); }, + BackgroundJob::AnimatePluginLoading(pid) => { + let loading_plugin = Arc::new(AtomicBool::new(true)); + if job_already_running(job, &mut running_jobs) { + continue; + } + task::spawn({ + let senders = bus.senders.clone(); + let loading_plugin = loading_plugin.clone(); + async move { + while loading_plugin.load(Ordering::SeqCst) { + let _ = senders.send_to_screen( + ScreenInstruction::ProgressPluginLoadingOffset(pid), + ); + task::sleep(std::time::Duration::from_millis( + PLUGIN_ANIMATION_OFFSET_DURATION_MD, + )) + .await; + } + } + }); + loading_plugins.insert(pid, loading_plugin); + }, + BackgroundJob::StopPluginLoadingAnimation(pid) => { + if let Some(loading_plugin) = loading_plugins.remove(&pid) { + loading_plugin.store(false, Ordering::SeqCst); + } + }, BackgroundJob::Exit => { + for loading_plugin in loading_plugins.values() { + loading_plugin.store(false, Ordering::SeqCst); + } return Ok(()); }, } diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index c6896f90d..28828907e 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -6,7 +6,10 @@ use crate::panes::{grid::Grid, sixel::SixelImageStore, LinkHandler, PaneId}; use crate::plugins::PluginInstruction; use crate::pty::VteBytes; use crate::tab::Pane; -use crate::ui::pane_boundaries_frame::{FrameParams, PaneFrame}; +use crate::ui::{ + loading_indication::LoadingIndication, + pane_boundaries_frame::{FrameParams, PaneFrame}, +}; use crate::ClientId; use std::cell::RefCell; use std::rc::Rc; @@ -67,6 +70,7 @@ pub(crate) struct PluginPane { borderless: bool, pane_frame_color_override: Option<(PaletteColor, Option)>, invoked_with: Option, + loading_indication: LoadingIndication, } impl PluginPane { @@ -81,10 +85,13 @@ impl PluginPane { terminal_emulator_color_codes: Rc>>, link_handler: Rc>, character_cell_size: Rc>>, + currently_connected_clients: Vec, style: Style, invoked_with: Option, ) -> Self { - Self { + let loading_indication = LoadingIndication::new(title.clone()).with_colors(style.colors); + let initial_loading_message = loading_indication.to_string(); + let mut plugin = PluginPane { pid, should_render: HashMap::new(), selectable: true, @@ -108,7 +115,12 @@ impl PluginPane { style, pane_frame_color_override: None, invoked_with, + loading_indication, + }; + for client_id in currently_connected_clients { + plugin.handle_plugin_bytes(client_id, initial_loading_message.as_bytes().to_vec()); } + plugin } } @@ -513,6 +525,24 @@ impl Pane for PluginPane { fn set_title(&mut self, title: String) { self.pane_title = title; } + fn update_loading_indication(&mut self, loading_indication: LoadingIndication) { + if self.loading_indication.ended { + return; + } + 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; + } + self.loading_indication.progress_animation_offset(); + self.handle_plugin_bytes_for_all_clients( + self.loading_indication.to_string().as_bytes().to_vec(), + ); + } } impl PluginPane { @@ -527,4 +557,10 @@ impl PluginPane { fn set_client_should_render(&mut self, client_id: ClientId, should_render: bool) { self.should_render.insert(client_id, should_render); } + fn handle_plugin_bytes_for_all_clients(&mut self, bytes: VteBytes) { + let client_ids: Vec = self.grids.keys().copied().collect(); + for client_id in client_ids { + self.handle_plugin_bytes(client_id, bytes.clone()); + } + } } diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 1cd034490..e1884b1e6 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -1,8 +1,10 @@ +mod start_plugin; 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 wasm_bridge::WasmBridge; @@ -20,7 +22,14 @@ use zellij_utils::{ #[derive(Clone, Debug)] pub enum PluginInstruction { - Load(RunPlugin, usize, ClientId, Size), // plugin metadata, tab_index, client_ids + Load( + Option, // should float + Option, // pane title + RunPlugin, + usize, // tab index + ClientId, + Size, + ), Update(Vec<(Option, Option, Event)>), // Focused plugin / broadcast, client_id, event data Unload(u32), // plugin_id Resize(u32, usize, usize), // plugin_id, columns, rows @@ -33,6 +42,7 @@ pub enum PluginInstruction { usize, // tab_index ClientId, ), + ApplyCachedEvents(u32), // u32 is the plugin id Exit, } @@ -47,6 +57,7 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::AddClient(_) => PluginContext::AddClient, PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient, PluginInstruction::NewTab(..) => PluginContext::NewTab, + PluginInstruction::ApplyCachedEvents(..) => PluginContext::ApplyCachedEvents, } } } @@ -69,8 +80,21 @@ pub(crate) fn plugin_thread_main( let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); err_ctx.add_call(ContextType::Plugin((&event).into())); match event { - PluginInstruction::Load(run, tab_index, client_id, size) => { - wasm_bridge.load_plugin(&run, tab_index, size, client_id)?; + PluginInstruction::Load(should_float, pane_title, run, tab_index, client_id, size) => { + 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}"); + }, + } }, PluginInstruction::Update(updates) => { wasm_bridge.update_plugins(updates)?; @@ -126,7 +150,13 @@ pub(crate) fn plugin_thread_main( client_id, ))); }, - PluginInstruction::Exit => break, + PluginInstruction::ApplyCachedEvents(plugin_id) => { + wasm_bridge.apply_cached_events(plugin_id)?; + }, + PluginInstruction::Exit => { + wasm_bridge.cleanup(); + break; + }, } } info!("wasm main thread exits"); diff --git a/zellij-server/src/plugins/start_plugin.rs b/zellij-server/src/plugins/start_plugin.rs new file mode 100644 index 000000000..28000bd15 --- /dev/null +++ b/zellij-server/src/plugins/start_plugin.rs @@ -0,0 +1,471 @@ +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 62e8cedb9..68f8bc6d9 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -1,11 +1,10 @@ use super::PluginInstruction; -use highway::{HighwayHash, PortableHash}; +use crate::plugins::start_plugin::start_plugin; use log::{debug, info, warn}; -use semver::Version; use serde::{de::DeserializeOwned, Serialize}; use std::{ collections::{HashMap, HashSet}, - fmt, fs, + fmt, path::PathBuf, process, str::FromStr, @@ -13,24 +12,25 @@ use std::{ thread, time::{Duration, Instant}, }; -use url::Url; use wasmer::{ imports, ChainableNamedResolver, Function, ImportObject, Instance, Module, Store, Value, WasmerEnv, }; -use wasmer_wasi::{Pipe, WasiEnv, WasiState}; +use wasmer_wasi::WasiEnv; +use zellij_utils::async_std::task::{self, JoinHandle}; use crate::{ - logging_pipe::LoggingPipe, + background_jobs::BackgroundJob, panes::PaneId, pty::{ClientOrTabIndex, PtyInstruction}, screen::ScreenInstruction, thread_bus::ThreadSenders, + ui::loading_indication::LoadingIndication, ClientId, }; use zellij_utils::{ - consts::{VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR}, + consts::VERSION, data::{Event, EventType, PluginIds}, errors::prelude::*, input::{ @@ -119,7 +119,7 @@ pub struct PluginEnv { pub tab_index: usize, pub client_id: ClientId, #[allow(dead_code)] - plugin_own_data_dir: PathBuf, + pub plugin_own_data_dir: PathBuf, } impl PluginEnv { @@ -133,21 +133,24 @@ impl PluginEnv { } } -type PluginMap = HashMap<(u32, ClientId), (Instance, PluginEnv, (usize, usize))>; // u32 => - // plugin_id, - // (usize, usize) - // => (rows, - // columns) +pub type PluginMap = HashMap<(u32, ClientId), (Instance, PluginEnv, (usize, usize))>; // u32 => + // plugin_id, + // (usize, usize) + // => (rows, + // columns) pub struct WasmBridge { - connected_clients: Vec, + connected_clients: Arc>>, plugins: PluginsConfig, senders: ThreadSenders, store: Store, plugin_dir: PathBuf, - plugin_cache: HashMap, - plugin_map: PluginMap, + plugin_cache: Arc>>, + plugin_map: Arc>, 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 } impl WasmBridge { @@ -157,9 +160,10 @@ impl WasmBridge { store: Store, plugin_dir: PathBuf, ) -> Self { - let plugin_map = HashMap::new(); - let connected_clients: Vec = vec![]; - let plugin_cache: HashMap = HashMap::new(); + let plugin_map = Arc::new(Mutex::new(HashMap::new())); + let connected_clients: Arc>> = Arc::new(Mutex::new(vec![])); + let plugin_cache: Arc>> = + Arc::new(Mutex::new(HashMap::new())); WasmBridge { connected_clients, plugins, @@ -169,6 +173,9 @@ impl WasmBridge { plugin_cache, plugin_map, next_plugin_id: 0, + cached_events_for_pending_plugins: HashMap::new(), + cached_resizes_for_pending_plugins: HashMap::new(), + loading_plugins: HashMap::new(), } } pub fn load_plugin( @@ -179,210 +186,96 @@ impl WasmBridge { client_id: ClientId, ) -> Result { // returns the plugin id - let err_context = || format!("failed to load plugin for client {client_id}"); + let err_context = move || format!("failed to load plugin for client {client_id}"); let plugin_id = self.next_plugin_id; let plugin = self .plugins .get(run) .with_context(|| format!("failed to resolve plugin {run:?}")) - .with_context(err_context) - .fatal(); - - let (instance, plugin_env) = self - .start_plugin(plugin_id, client_id, &plugin, tab_index) .with_context(err_context)?; + let plugin_name = run.location.to_string(); - let mut main_user_instance = instance.clone(); - let main_user_env = plugin_env.clone(); - load_plugin_instance(&mut main_user_instance).with_context(err_context)?; - - self.plugin_map.insert( - (plugin_id, client_id), - (main_user_instance, main_user_env, (size.rows, size.cols)), - ); + self.next_plugin_id += 1; - // clone plugins for the rest of the client ids if they exist - for client_id in self.connected_clients.iter() { - 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(&self.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)?; - self.plugin_map.insert( - (plugin_id, *client_id), - (instance, new_plugin_env, (size.rows, size.cols)), - ); - } + self.cached_events_for_pending_plugins + .insert(plugin_id, vec![]); + self.cached_resizes_for_pending_plugins + .insert(plugin_id, (0, 0)); + + 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 { + let _ = + senders.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id)); + let mut loading_indication = LoadingIndication::new(plugin_name.clone()); + match start_plugin( + plugin_id, + client_id, + &plugin, + tab_index, + plugin_dir, + plugin_cache, + senders.clone(), + store, + plugin_map, + size, + 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(), + )); + }, + } + } + }); + self.loading_plugins.insert(plugin_id, load_plugin_task); self.next_plugin_id += 1; Ok(plugin_id) } pub fn unload_plugin(&mut self, pid: u32) -> Result<()> { info!("Bye from plugin {}", &pid); // TODO: remove plugin's own data directory - let ids_in_plugin_map: Vec<(u32, ClientId)> = self.plugin_map.keys().copied().collect(); + let mut plugin_map = self.plugin_map.lock().unwrap(); + let ids_in_plugin_map: Vec<(u32, ClientId)> = plugin_map.keys().copied().collect(); for (plugin_id, client_id) in ids_in_plugin_map { if pid == plugin_id { - drop(self.plugin_map.remove(&(plugin_id, client_id))); + drop(plugin_map.remove(&(plugin_id, client_id))); } } Ok(()) } - #[allow(clippy::too_many_arguments)] - pub fn start_plugin( - &mut self, - plugin_id: u32, - client_id: ClientId, - plugin: &PluginConfig, - tab_index: usize, - ) -> Result<(Instance, PluginEnv)> { - 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()); - let cache_hit = self.plugin_cache.contains_key(&plugin.path); - - // 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)?; - - // We remove the entry here and repopulate it at the very bottom, if everything went well. - // We must do that because a `get` will only give us a borrow of the Module. This suffices for - // the purpose of setting everything up, but we cannot return a &Module from the "None" match - // arm, because we create the Module from scratch there. Any reference passed outside would - // outlive the Module we create there. Hence, we remove the plugin here and reinsert it - // below... - let module = match self.plugin_cache.remove(&plugin.path) { - Some(module) => { - log::debug!( - "Loaded plugin '{}' from plugin cache", - plugin.path.display() - ); - module - }, - None => { - // 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(&self.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); - - let timer = std::time::Instant::now(); - unsafe { - match Module::deserialize_from_file(&self.store, &cached_path) { - Ok(m) => { - log::info!( - "Loaded plugin '{}' from cache folder at '{}' in {:?}", - plugin.path.display(), - ZELLIJ_CACHE_DIR.display(), - timer.elapsed(), - ); - m - }, - Err(e) => { - let inner_context = || format!("failed to recover from {e:?}"); - - fs::create_dir_all(ZELLIJ_CACHE_DIR.to_owned()) - .map_err(anyError::new) - .and_then(|_| { - Module::new(&self.store, &wasm_bytes).map_err(anyError::new) - }) - .and_then(|m| { - m.serialize_to_file(&cached_path).map_err(anyError::new)?; - log::info!( - "Compiled plugin '{}' in {:?}", - plugin.path.display(), - timer.elapsed() - ); - Ok(m) - }) - .with_context(inner_context) - .with_context(err_context)? - }, - } - } - }, - }; - - 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: self.senders.clone(), - wasi_env, - subscriptions: Arc::new(Mutex::new(HashSet::new())), - plugin_own_data_dir, - tab_index, - }; - - let zellij = zellij_exports(&self.store, &plugin_env); - let instance = - Instance::new(&module, &zellij.chain_back(wasi)).with_context(err_context)?; - - 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(); - self.plugin_cache.insert(cloned_plugin.path, module); - - Ok((instance, plugin_env)) - } pub fn add_client(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to add plugins for client {client_id}"); - self.connected_clients.push(client_id); + self.connected_clients.lock().unwrap().push(client_id); let mut seen = HashSet::new(); let mut new_plugins = HashMap::new(); - for (&(plugin_id, _), (instance, plugin_env, (rows, columns))) in &self.plugin_map { + let mut plugin_map = self.plugin_map.lock().unwrap(); + for (&(plugin_id, _), (instance, plugin_env, (rows, columns))) in &*plugin_map { if seen.contains(&plugin_id) { continue; } @@ -404,7 +297,7 @@ impl WasmBridge { let mut instance = Instance::new(&module, &zellij.chain_back(wasi)).with_context(err_context)?; load_plugin_instance(&mut instance).with_context(err_context)?; - self.plugin_map.insert( + plugin_map.insert( (plugin_id, client_id), (instance, new_plugin_env, (rows, columns)), ); @@ -414,8 +307,9 @@ impl WasmBridge { pub fn resize_plugin(&mut self, pid: u32, new_columns: usize, new_rows: usize) -> Result<()> { let err_context = || format!("failed to resize plugin {pid}"); let mut plugin_bytes = vec![]; + let mut plugin_map = self.plugin_map.lock().unwrap(); for ((plugin_id, client_id), (instance, plugin_env, (current_rows, current_columns))) in - self.plugin_map.iter_mut() + plugin_map.iter_mut() { if *plugin_id == pid { *current_rows = new_rows; @@ -440,6 +334,14 @@ 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() + { + if *plugin_id == pid { + *current_rows = new_rows; + *current_columns = new_columns; + } + } let _ = self .senders .send_to_screen(ScreenInstruction::PluginBytes(plugin_bytes)); @@ -451,11 +353,10 @@ impl WasmBridge { ) -> Result<()> { let err_context = || "failed to update plugin state".to_string(); + let plugin_map = self.plugin_map.lock().unwrap(); let mut plugin_bytes = vec![]; for (pid, cid, event) in updates.drain(..) { - for (&(plugin_id, client_id), (instance, plugin_env, (rows, columns))) in - &self.plugin_map - { + for (&(plugin_id, client_id), (instance, plugin_env, (rows, columns))) in &*plugin_map { let subs = plugin_env .subscriptions .lock() @@ -470,102 +371,90 @@ impl WasmBridge { || (cid.is_none() && pid == Some(plugin_id)) || (cid == Some(client_id) && pid == Some(plugin_id))) { - let update = instance - .exports - .get_function("update") + apply_event_to_plugin( + plugin_id, + client_id, + &instance, + &plugin_env, + &event, + *rows, + *columns, + &mut plugin_bytes, + )?; + } + } + for (plugin_id, cached_events) in self.cached_events_for_pending_plugins.iter_mut() { + if pid.is_none() || pid.as_ref() == Some(plugin_id) { + cached_events.push(event.clone()); + } + } + } + let _ = self + .senders + .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}"); + 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 + .connected_clients + .lock() + .unwrap() + .iter() + .copied() + .collect(); + 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)) + { + let subs = plugin_env + .subscriptions + .lock() + .to_anyhow() .with_context(err_context)?; - wasi_write_object(&plugin_env.wasi_env, &event).with_context(err_context)?; - let update_return = update.call(&[]).or_else::(|e| { - match e.downcast::() { - Ok(_) => panic!( - "{}", - anyError::new(VersionMismatchError::new( - VERSION, - "Unavailable", - &plugin_env.plugin.path, - plugin_env.plugin.is_builtin(), - )) - ), - Err(e) => Err(e).with_context(err_context), + for event in events.clone() { + let event_type = + EventType::from_str(&event.to_string()).with_context(err_context)?; + if !subs.contains(&event_type) { + continue; } - })?; - let should_render = match update_return.get(0) { - Some(Value::I32(n)) => *n == 1, - _ => false, - }; - - if *rows > 0 && *columns > 0 && should_render { - let rendered_bytes = instance - .exports - .get_function("render") - .map_err(anyError::new) - .and_then(|render| { - render - .call(&[Value::I32(*rows as i32), Value::I32(*columns as i32)]) - .map_err(anyError::new) - }) - .and_then(|_| wasi_read_string(&plugin_env.wasi_env)) - .with_context(err_context)?; - plugin_bytes.push(( + apply_event_to_plugin( plugin_id, client_id, - rendered_bytes.as_bytes().to_vec(), - )); + &instance, + &plugin_env, + &event, + *rows, + *columns, + &mut plugin_bytes, + )?; } + let _ = self + .senders + .send_to_screen(ScreenInstruction::PluginBytes(plugin_bytes)); } } } - let _ = self - .senders - .send_to_screen(ScreenInstruction::PluginBytes(plugin_bytes)); + 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.retain(|c| c != &client_id); + self.connected_clients + .lock() + .unwrap() + .retain(|c| c != &client_id); } -} - -// 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(), - ))); + pub fn cleanup(&mut self) { + for (_plugin_id, loading_plugin_task) in self.loading_plugins.drain() { + drop(loading_plugin_task.cancel()); + } } - - Ok(()) } fn load_plugin_instance(instance: &mut Instance) -> Result<()> { @@ -853,3 +742,56 @@ pub fn wasi_read_object(wasi_env: &WasiEnv) -> Result { .and_then(|string| serde_json::from_str(&string).map_err(anyError::new)) .with_context(|| format!("failed to deserialize object from WASI env '{wasi_env:?}'")) } + +pub fn apply_event_to_plugin( + plugin_id: u32, + client_id: ClientId, + instance: &Instance, + plugin_env: &PluginEnv, + event: &Event, + rows: usize, + columns: usize, + plugin_bytes: &mut Vec<(u32, ClientId, Vec)>, +) -> Result<()> { + let err_context = || format!("Failed to apply event to plugin {plugin_id}"); + let update = instance + .exports + .get_function("update") + .with_context(err_context)?; + wasi_write_object(&plugin_env.wasi_env, &event).with_context(err_context)?; + let update_return = + update + .call(&[]) + .or_else::(|e| match e.downcast::() { + Ok(_) => panic!( + "{}", + anyError::new(VersionMismatchError::new( + VERSION, + "Unavailable", + &plugin_env.plugin.path, + plugin_env.plugin.is_builtin(), + )) + ), + Err(e) => Err(e).with_context(err_context), + })?; + let should_render = match update_return.get(0) { + Some(Value::I32(n)) => *n == 1, + _ => false, + }; + + if rows > 0 && columns > 0 && should_render { + let rendered_bytes = instance + .exports + .get_function("render") + .map_err(anyError::new) + .and_then(|render| { + render + .call(&[Value::I32(rows as i32), Value::I32(columns as i32)]) + .map_err(anyError::new) + }) + .and_then(|_| wasi_read_string(&plugin_env.wasi_env)) + .with_context(err_context)?; + plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec())); + } + Ok(()) +} diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 53b43ecdc..f63da2d21 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -667,6 +667,22 @@ pub(crate) fn route_action( .send_to_screen(ScreenInstruction::QueryTabNames(client_id)) .with_context(err_context)?; }, + Action::NewTiledPluginPane(run_plugin, name) => { + session + .senders + .send_to_screen(ScreenInstruction::NewTiledPluginPane( + run_plugin, name, client_id, + )) + .with_context(err_context)?; + }, + Action::NewFloatingPluginPane(run_plugin, name) => { + session + .senders + .send_to_screen(ScreenInstruction::NewFloatingPluginPane( + run_plugin, name, client_id, + )) + .with_context(err_context)?; + }, } Ok(should_break) } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 5a4223142..f1dd38741 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -13,7 +13,8 @@ use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::{ input::command::TerminalAction, input::layout::{ - FloatingPaneLayout, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, + FloatingPaneLayout, Run, RunPlugin, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout, + TiledPaneLayout, }, position::Position, }; @@ -29,7 +30,10 @@ use crate::{ pty::{ClientOrTabIndex, PtyInstruction, VteBytes}, tab::Tab, thread_bus::Bus, - ui::overlay::{Overlay, OverlayWindow, Overlayable}, + ui::{ + loading_indication::LoadingIndication, + overlay::{Overlay, OverlayWindow, Overlayable}, + }, ClientId, ServerInstruction, }; use zellij_utils::{ @@ -250,6 +254,18 @@ pub enum ScreenInstruction { PreviousSwapLayout(ClientId), NextSwapLayout(ClientId), QueryTabNames(ClientId), + NewTiledPluginPane(RunPluginLocation, Option, ClientId), // Option is + NewFloatingPluginPane(RunPluginLocation, Option, ClientId), // Option is an + // optional pane title + AddPlugin( + Option, // should_float + RunPlugin, + Option, // pane title + usize, // tab index + u32, // plugin id + ), + UpdatePluginLoadingStage(u32, LoadingIndication), // u32 - plugin_id + ProgressPluginLoadingOffset(u32), // u32 - plugin id } impl From<&ScreenInstruction> for ScreenContext { @@ -393,6 +409,15 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::PreviousSwapLayout(..) => ScreenContext::PreviousSwapLayout, ScreenInstruction::NextSwapLayout(..) => ScreenContext::NextSwapLayout, ScreenInstruction::QueryTabNames(..) => ScreenContext::QueryTabNames, + ScreenInstruction::NewTiledPluginPane(..) => ScreenContext::NewTiledPluginPane, + ScreenInstruction::NewFloatingPluginPane(..) => ScreenContext::NewFloatingPluginPane, + ScreenInstruction::AddPlugin(..) => ScreenContext::AddPlugin, + ScreenInstruction::UpdatePluginLoadingStage(..) => { + ScreenContext::UpdatePluginLoadingStage + }, + ScreenInstruction::ProgressPluginLoadingOffset(..) => { + ScreenContext::ProgressPluginLoadingOffset + }, } } } @@ -2426,6 +2451,89 @@ pub(crate) fn screen_thread_main( .senders .send_to_server(ServerInstruction::Log(tab_names, client_id))?; }, + ScreenInstruction::NewTiledPluginPane(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::Load( + should_float, + pane_title, + run_plugin, + *tab_index, + client_id, + size, + ))?; + }, + ScreenInstruction::NewFloatingPluginPane( + run_plugin_location, + pane_title, + client_id, + ) => { + let tab_index = screen.active_tab_indices.values().next().unwrap(); // TODO: no + // unwrap and + // better + let size = Size::default(); // TODO: ??? + let should_float = Some(true); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: run_plugin_location, + }; + screen.bus.senders.send_to_plugin(PluginInstruction::Load( + should_float, + pane_title, + run_plugin, + *tab_index, + client_id, + size, + ))?; + }, + ScreenInstruction::AddPlugin( + should_float, + run_plugin_location, + pane_title, + tab_index, + plugin_id, + ) => { + let pane_title = + 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( + PaneId::Plugin(plugin_id), + pane_title, + should_float, + run_plugin, + None, + )?; + } else { + log::error!("Tab index not found: {:?}", tab_index); + } + screen.unblock_input()?; + }, + ScreenInstruction::UpdatePluginLoadingStage(pid, loading_indication) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_plugin(pid) { + tab.update_plugin_loading_stage(pid, loading_indication); + break; + } + } + screen.render()?; + }, + ScreenInstruction::ProgressPluginLoadingOffset(pid) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_plugin(pid) { + tab.progress_plugin_loading_offset(pid); + break; + } + } + screen.render()?; + }, } } Ok(()) diff --git a/zellij-server/src/tab/layout_applier.rs b/zellij-server/src/tab/layout_applier.rs index 7964b6989..04c22fe9a 100644 --- a/zellij-server/src/tab/layout_applier.rs +++ b/zellij-server/src/tab/layout_applier.rs @@ -14,7 +14,7 @@ use crate::{ ClientId, }; use std::cell::RefCell; -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::rc::Rc; use zellij_utils::{ data::{Palette, Style}, @@ -30,6 +30,7 @@ pub struct LayoutApplier<'a> { terminal_emulator_colors: Rc>, terminal_emulator_color_codes: Rc>>, character_cell_size: Rc>>, + connected_clients: Rc>>, style: Style, display_area: Rc>, // includes all panes (including eg. the status bar and tab bar in the default la