diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 07e8d051..63688986 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -4,6 +4,7 @@ pub mod panes; pub mod tab; mod logging_pipe; +mod plugins; mod pty; mod pty_writer; mod route; @@ -11,7 +12,6 @@ mod screen; mod terminal_bytes; mod thread_bus; mod ui; -mod wasm_vm; use log::info; use pty_writer::{pty_writer_main, PtyWriteInstruction}; @@ -29,10 +29,10 @@ use wasmer::Store; use crate::{ os_input_output::ServerOsApi, + plugins::{plugin_thread_main, PluginInstruction}, pty::{pty_thread_main, Pty, PtyInstruction}, screen::{screen_thread_main, ScreenInstruction}, thread_bus::{Bus, ThreadSenders}, - wasm_vm::{wasm_thread_main, PluginInstruction}, }; use route::route_thread_main; use zellij_utils::{ @@ -108,7 +108,7 @@ pub(crate) struct SessionMetaData { pub default_shell: Option, screen_thread: Option>, pty_thread: Option>, - wasm_thread: Option>, + plugin_thread: Option>, pty_writer_thread: Option>, } @@ -124,8 +124,8 @@ impl Drop for SessionMetaData { if let Some(pty_thread) = self.pty_thread.take() { let _ = pty_thread.join(); } - if let Some(wasm_thread) = self.wasm_thread.take() { - let _ = wasm_thread.join(); + if let Some(plugin_thread) = self.plugin_thread.take() { + let _ = plugin_thread.join(); } if let Some(pty_writer_thread) = self.pty_writer_thread.take() { let _ = pty_writer_thread.join(); @@ -332,7 +332,7 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .as_ref() .unwrap() .senders - .send_to_pty(PtyInstruction::NewTab( + .send_to_screen(ScreenInstruction::NewTab( default_shell.clone(), tab_layout, tab_name, @@ -655,6 +655,7 @@ fn init_session( let pty_thread = thread::Builder::new() .name("pty".to_string()) .spawn({ + let layout = layout.clone(); let pty = Pty::new( Bus::new( vec![pty_receiver], @@ -700,7 +701,7 @@ fn init_session( }) .unwrap(); - let wasm_thread = thread::Builder::new() + let plugin_thread = thread::Builder::new() .name("wasm".to_string()) .spawn({ let plugin_bus = Bus::new( @@ -715,7 +716,14 @@ fn init_session( let store = Store::default(); move || { - wasm_thread_main(plugin_bus, store, data_dir, plugins.unwrap_or_default()).fatal() + plugin_thread_main( + plugin_bus, + store, + data_dir, + plugins.unwrap_or_default(), + layout, + ) + .fatal() } }) .unwrap(); @@ -750,7 +758,7 @@ fn init_session( client_attributes, screen_thread: Some(screen_thread), pty_thread: Some(pty_thread), - wasm_thread: Some(wasm_thread), + plugin_thread: Some(plugin_thread), pty_writer_thread: Some(pty_writer_thread), } } diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 3ca7f572..bb446e8d 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -9,9 +9,9 @@ use crate::{ os_input_output::ServerOsApi, output::{FloatingPanesStack, Output}, panes::{ActivePanes, PaneId}, + plugins::PluginInstruction, thread_bus::ThreadSenders, ui::pane_contents_and_ui::PaneContentsAndUi, - wasm_vm::PluginInstruction, ClientId, }; use std::cell::RefCell; diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 9353b511..30624d05 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -3,10 +3,10 @@ use std::time::Instant; use crate::output::{CharacterChunk, SixelImageChunk}; 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::wasm_vm::PluginInstruction; use crate::ClientId; use std::cell::RefCell; use std::rc::Rc; diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index b1665cc9..e49c8654 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -8,11 +8,11 @@ use crate::{ os_input_output::ServerOsApi, output::Output, panes::{ActivePanes, PaneId}, + plugins::PluginInstruction, tab::{Pane, MIN_TERMINAL_HEIGHT, MIN_TERMINAL_WIDTH}, thread_bus::ThreadSenders, ui::boundaries::Boundaries, ui::pane_contents_and_ui::PaneContentsAndUi, - wasm_vm::PluginInstruction, ClientId, }; use zellij_utils::{ @@ -346,9 +346,7 @@ impl TiledPanes { self.reset_boundaries(); } pub fn focus_pane_if_client_not_focused(&mut self, pane_id: PaneId, client_id: ClientId) { - log::info!("inside focus_pane_if_client_not_focused"); if self.active_panes.get(&client_id).is_none() { - log::info!("is none"); self.focus_pane(pane_id, client_id) } } diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs new file mode 100644 index 00000000..c5f3781e --- /dev/null +++ b/zellij-server/src/plugins/mod.rs @@ -0,0 +1,135 @@ +mod wasm_bridge; +use log::info; +use std::{collections::HashMap, fs, path::PathBuf}; +use wasmer::Store; + +use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId}; + +use wasm_bridge::WasmBridge; + +use zellij_utils::{ + data::Event, + errors::{prelude::*, ContextType, PluginContext}, + input::{ + command::TerminalAction, + layout::{Layout, PaneLayout, Run, RunPlugin, RunPluginLocation}, + plugins::PluginsConfig, + }, + pane_size::Size, +}; + +#[derive(Clone, Debug)] +pub enum PluginInstruction { + Load(RunPlugin, usize, ClientId, Size), // plugin metadata, tab_index, client_ids + Update(Option, Option, Event), // Focused plugin / broadcast, client_id, event data + Unload(u32), // plugin_id + Resize(u32, usize, usize), // plugin_id, columns, rows + AddClient(ClientId), + RemoveClient(ClientId), + NewTab( + Option, + Option, + Option, // tab name + usize, // tab_index + ClientId, + ), + Exit, +} + +impl From<&PluginInstruction> for PluginContext { + fn from(plugin_instruction: &PluginInstruction) -> Self { + match *plugin_instruction { + PluginInstruction::Load(..) => PluginContext::Load, + PluginInstruction::Update(..) => PluginContext::Update, + PluginInstruction::Unload(..) => PluginContext::Unload, + PluginInstruction::Resize(..) => PluginContext::Resize, + PluginInstruction::Exit => PluginContext::Exit, + PluginInstruction::AddClient(_) => PluginContext::AddClient, + PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient, + PluginInstruction::NewTab(..) => PluginContext::NewTab, + } + } +} + +pub(crate) fn plugin_thread_main( + bus: Bus, + store: Store, + data_dir: PathBuf, + plugins: PluginsConfig, + layout: Box, +) -> 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); + + loop { + let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Plugin((&event).into())); + match event { + // TODO: remove pid_tx from here + PluginInstruction::Load(run, tab_index, client_id, size) => { + wasm_bridge.load_plugin(&run, tab_index, size, client_id)?; + }, + PluginInstruction::Update(pid, cid, event) => { + wasm_bridge.update_plugins(pid, cid, event)?; + }, + PluginInstruction::Unload(pid) => { + wasm_bridge.unload_plugin(pid)?; + }, + PluginInstruction::Resize(pid, new_columns, new_rows) => { + wasm_bridge.resize_plugin(pid, new_columns, new_rows)?; + }, + PluginInstruction::AddClient(client_id) => { + wasm_bridge.add_client(client_id)?; + }, + PluginInstruction::RemoveClient(client_id) => { + wasm_bridge.remove_client(client_id); + }, + PluginInstruction::NewTab( + terminal_action, + tab_layout, + tab_name, + tab_index, + client_id, + ) => { + let mut plugin_ids: HashMap> = HashMap::new(); + let extracted_run_instructions = tab_layout + .clone() + .unwrap_or_else(|| layout.new_tab()) + .extract_run_instructions(); + let size = Size::default(); // TODO: is this bad? + for run_instruction in extracted_run_instructions { + if let Some(Run::Plugin(run)) = run_instruction { + let plugin_id = + wasm_bridge.load_plugin(&run, tab_index, size, client_id)?; + plugin_ids.entry(run.location).or_default().push(plugin_id); + } + } + drop(bus.senders.send_to_pty(PtyInstruction::NewTab( + terminal_action, + tab_layout, + tab_name, + tab_index, + plugin_ids, + client_id, + ))); + }, + PluginInstruction::Exit => break, + } + } + info!("wasm main thread exits"); + + fs::remove_dir_all(&plugin_global_data_dir) + .or_else(|err| { + if err.kind() == std::io::ErrorKind::NotFound { + // I don't care... + Ok(()) + } else { + Err(err) + } + }) + .context("failed to cleanup plugin data directory") +} diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs new file mode 100644 index 00000000..374b1dd4 --- /dev/null +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -0,0 +1,737 @@ +use super::PluginInstruction; +use highway::{HighwayHash, PortableHash}; +use log::{debug, info, warn}; +use semver::Version; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + collections::{HashMap, HashSet}, + fmt, fs, + path::PathBuf, + process, + str::FromStr, + sync::{Arc, Mutex}, + 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 crate::{ + logging_pipe::LoggingPipe, + panes::PaneId, + pty::{ClientOrTabIndex, PtyInstruction}, + screen::ScreenInstruction, + thread_bus::ThreadSenders, + ClientId, +}; + +use zellij_utils::{ + consts::{DEBUG_MODE, VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR}, + data::{Event, EventType, PluginIds}, + errors::prelude::*, + input::{ + command::TerminalAction, + layout::RunPlugin, + plugins::{PluginConfig, PluginType, PluginsConfig}, + }, + pane_size::Size, + serde, +}; + +/// Custom error for plugin version mismatch. +/// +/// This is thrown when, during starting a plugin, it is detected that the plugin version doesn't +/// match the zellij version. This is treated as a fatal error and leads to instantaneous +/// termination. +#[derive(Debug)] +pub struct VersionMismatchError { + zellij_version: String, + plugin_version: String, + plugin_path: PathBuf, + // true for builtin plugins + builtin: bool, +} + +impl std::error::Error for VersionMismatchError {} + +impl VersionMismatchError { + pub fn new( + zellij_version: &str, + plugin_version: &str, + plugin_path: &PathBuf, + builtin: bool, + ) -> Self { + VersionMismatchError { + zellij_version: zellij_version.to_owned(), + plugin_version: plugin_version.to_owned(), + plugin_path: plugin_path.to_owned(), + builtin, + } + } +} + +impl fmt::Display for VersionMismatchError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let first_line = if self.builtin { + "It seems your version of zellij was built with outdated core plugins." + } else { + "If you're seeing this error a plugin version doesn't match the current +zellij version." + }; + + write!( + f, + "{} +Detected versions: + +- Plugin version: {} +- Zellij version: {} +- Offending plugin: {} + +If you're a user: + Please contact the distributor of your zellij version and report this error + to them. + +If you're a developer: + Please run zellij with updated plugins. The easiest way to achieve this + is to build zellij with `cargo make 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() + ) + } +} + +#[derive(WasmerEnv, Clone)] +pub struct PluginEnv { + pub plugin_id: u32, + pub plugin: PluginConfig, + pub senders: ThreadSenders, + pub wasi_env: WasiEnv, + pub subscriptions: Arc>>, + pub tab_index: usize, + pub client_id: ClientId, + #[allow(dead_code)] + plugin_own_data_dir: PathBuf, +} + +type PluginMap = HashMap<(u32, ClientId), (Instance, PluginEnv, (usize, usize))>; // u32 => + // plugin_id, + // (usize, usize) + // => (rows, + // columns) + +pub struct WasmBridge { + connected_clients: Vec, + plugins: PluginsConfig, + senders: ThreadSenders, + store: Store, + plugin_dir: PathBuf, + plugin_cache: HashMap, + plugin_map: PluginMap, + next_plugin_id: u32, +} + +impl WasmBridge { + pub fn new( + plugins: PluginsConfig, + senders: ThreadSenders, + store: Store, + plugin_dir: PathBuf, + ) -> Self { + let plugin_map = HashMap::new(); + let connected_clients: Vec = vec![]; + let plugin_cache: HashMap = HashMap::new(); + WasmBridge { + connected_clients, + plugins, + senders, + store, + plugin_dir, + plugin_cache, + plugin_map, + next_plugin_id: 0, + } + } + pub fn load_plugin( + &mut self, + run: &RunPlugin, + tab_index: usize, + size: Size, + client_id: ClientId, + ) -> Result { + // returns the plugin id + let err_context = || 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 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)), + ); + + // 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.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(); + for (plugin_id, client_id) in ids_in_plugin_map { + if pid == plugin_id { + drop(self.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); + + // 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(); + + fs::create_dir_all(&plugin_own_data_dir) + .with_context(|| format!("failed to create datadir in {plugin_own_data_dir:?}")) + .with_context(err_context) + .non_fatal(); + + // ensure tmp dir exists, in case it somehow was deleted (e.g systemd-tmpfiles) + 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) + .non_fatal(); + + let hash: String = PortableHash::default() + .hash256(&wasm_bytes) + .iter() + .map(ToString::to_string) + .collect(); + let cached_path = ZELLIJ_CACHE_DIR.join(&hash); + + unsafe { + match Module::deserialize_from_file(&self.store, &cached_path) { + Ok(m) => { + log::debug!( + "Loaded plugin '{}' from cache folder at '{}'", + plugin.path.display(), + ZELLIJ_CACHE_DIR.display(), + ); + m + }, + Err(e) => { + let inner_context = || format!("failed to recover from {e:?}"); + + let m = Module::new(&self.store, &wasm_bytes) + .with_context(inner_context) + .with_context(err_context)?; + fs::create_dir_all(ZELLIJ_CACHE_DIR.to_owned()) + .with_context(inner_context) + .with_context(err_context)?; + m.serialize_to_file(&cached_path) + .with_context(inner_context) + .with_context(err_context)?; + m + }, + } + } + }, + }; + + 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); + + let mut seen = HashSet::new(); + let mut new_plugins = HashMap::new(); + for (&(plugin_id, _), (instance, plugin_env, (rows, columns))) in &self.plugin_map { + if seen.contains(&plugin_id) { + continue; + } + seen.insert(plugin_id); + let mut new_plugin_env = plugin_env.clone(); + + new_plugin_env.client_id = client_id; + new_plugins.insert( + plugin_id, + (instance.module().clone(), new_plugin_env, (*rows, *columns)), + ); + } + for (plugin_id, (module, mut new_plugin_env, (rows, columns))) in new_plugins.drain() { + 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, (rows, columns)), + ); + } + Ok(()) + } + pub fn resize_plugin(&mut self, pid: u32, new_columns: usize, new_rows: usize) -> Result<()> { + let err_context = || format!("failed to resize plugin {pid}"); + for ((plugin_id, client_id), (instance, plugin_env, (current_rows, current_columns))) in + self.plugin_map.iter_mut() + { + if *plugin_id == pid { + *current_rows = new_rows; + *current_columns = new_columns; + + // TODO: consolidate with above render function + let render = instance + .exports + .get_function("render") + .with_context(err_context)?; + + render + .call(&[ + Value::I32(*current_rows as i32), + Value::I32(*current_columns as i32), + ]) + .with_context(err_context)?; + let rendered_bytes = wasi_read_string(&plugin_env.wasi_env); + drop(self.senders.send_to_screen(ScreenInstruction::PluginBytes( + *plugin_id, + *client_id, + rendered_bytes.as_bytes().to_vec(), + ))); + } + } + Ok(()) + } + pub fn update_plugins( + &mut self, + pid: Option, + cid: Option, + event: Event, + ) -> Result<()> { + let err_context = || { + if *DEBUG_MODE.get().unwrap_or(&true) { + format!("failed to update plugin state with event: {event:#?}") + } else { + "failed to update plugin state".to_string() + } + }; + + for (&(plugin_id, client_id), (instance, plugin_env, (rows, columns))) in &self.plugin_map { + let subs = plugin_env + .subscriptions + .lock() + .to_anyhow() + .with_context(err_context)?; + // FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType? + let event_type = EventType::from_str(&event.to_string()).with_context(err_context)?; + if subs.contains(&event_type) + && ((pid.is_none() && cid.is_none()) + || (pid.is_none() && cid == Some(client_id)) + || (cid.is_none() && pid == Some(plugin_id)) + || (cid == Some(client_id) && pid == Some(plugin_id))) + { + let update = instance + .exports + .get_function("update") + .with_context(err_context)?; + wasi_write_object(&plugin_env.wasi_env, &event); + 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 render = instance + .exports + .get_function("render") + .with_context(err_context)?; + render + .call(&[Value::I32(*rows as i32), Value::I32(*columns as i32)]) + .with_context(err_context)?; + let rendered_bytes = wasi_read_string(&plugin_env.wasi_env); + drop(self.senders.send_to_screen(ScreenInstruction::PluginBytes( + plugin_id, + client_id, + rendered_bytes.as_bytes().to_vec(), + ))); + } + } + } + Ok(()) + } + pub fn remove_client(&mut self, client_id: ClientId) { + self.connected_clients.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(), + ))) + }, + }; + plugin_version_func.call(&[]).with_context(err_context)?; + let plugin_version_str = wasi_read_string(&plugin_env.wasi_env); + let plugin_version = Version::parse(&plugin_version_str) + .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_str, + &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(crate) fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObject { + macro_rules! zellij_export { + ($($host_function:ident),+ $(,)?) => { + imports! { + "zellij" => { + $(stringify!($host_function) => + Function::new_native_with_env(store, plugin_env.clone(), $host_function),)+ + } + } + } + } + + zellij_export! { + host_subscribe, + host_unsubscribe, + host_set_selectable, + host_get_plugin_ids, + host_get_zellij_version, + host_open_file, + host_switch_tab_to, + host_set_timeout, + host_exec_cmd, + host_report_panic, + } +} + +fn host_subscribe(plugin_env: &PluginEnv) { + let mut subscriptions = plugin_env.subscriptions.lock().unwrap(); + let new: HashSet = wasi_read_object(&plugin_env.wasi_env); + subscriptions.extend(new); +} + +fn host_unsubscribe(plugin_env: &PluginEnv) { + let mut subscriptions = plugin_env.subscriptions.lock().unwrap(); + let old: HashSet = wasi_read_object(&plugin_env.wasi_env); + subscriptions.retain(|k| !old.contains(k)); +} + +fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) { + match plugin_env.plugin.run { + PluginType::Pane(Some(tab_index)) => { + let selectable = selectable != 0; + plugin_env + .senders + .send_to_screen(ScreenInstruction::SetSelectable( + PaneId::Plugin(plugin_env.plugin_id), + selectable, + tab_index, + )) + .unwrap() + }, + _ => { + debug!( + "{} - Calling method 'host_set_selectable' does nothing for headless plugins", + plugin_env.plugin.location + ) + }, + } +} + +fn host_get_plugin_ids(plugin_env: &PluginEnv) { + let ids = PluginIds { + plugin_id: plugin_env.plugin_id, + zellij_pid: process::id(), + }; + wasi_write_object(&plugin_env.wasi_env, &ids); +} + +fn host_get_zellij_version(plugin_env: &PluginEnv) { + wasi_write_object(&plugin_env.wasi_env, VERSION); +} + +fn host_open_file(plugin_env: &PluginEnv) { + let path: PathBuf = wasi_read_object(&plugin_env.wasi_env); + plugin_env + .senders + .send_to_pty(PtyInstruction::SpawnTerminal( + Some(TerminalAction::OpenFile(path, None)), + None, + None, + ClientOrTabIndex::TabIndex(plugin_env.tab_index), + )) + .unwrap(); +} + +fn host_switch_tab_to(plugin_env: &PluginEnv, tab_idx: u32) { + plugin_env + .senders + .send_to_screen(ScreenInstruction::GoToTab( + tab_idx, + Some(plugin_env.client_id), + )) + .unwrap(); +} + +fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) { + // There is a fancy, high-performance way to do this with zero additional threads: + // If the plugin thread keeps a BinaryHeap of timer structs, it can manage multiple and easily `.peek()` at the + // next time to trigger in O(1) time. Once the wake-up time is known, the `wasm` thread can use `recv_timeout()` + // to wait for an event with the timeout set to be the time of the next wake up. If events come in in the meantime, + // they are handled, but if the timeout triggers, we replace the event from `recv()` with an + // `Update(pid, TimerEvent)` and pop the timer from the Heap (or reschedule it). No additional threads for as many + // timers as we'd like. + // + // But that's a lot of code, and this is a few lines: + let send_plugin_instructions = plugin_env.senders.to_plugin.clone(); + let update_target = Some(plugin_env.plugin_id); + let client_id = plugin_env.client_id; + thread::spawn(move || { + let start_time = Instant::now(); + thread::sleep(Duration::from_secs_f64(secs)); + // FIXME: The way that elapsed time is being calculated here is not exact; it doesn't take into account the + // time it takes an event to actually reach the plugin after it's sent to the `wasm` thread. + let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64(); + + send_plugin_instructions + .unwrap() + .send(PluginInstruction::Update( + update_target, + Some(client_id), + Event::Timer(elapsed_time), + )) + .unwrap(); + }); +} + +fn host_exec_cmd(plugin_env: &PluginEnv) { + let mut cmdline: Vec = wasi_read_object(&plugin_env.wasi_env); + let command = cmdline.remove(0); + + // Bail out if we're forbidden to run command + if !plugin_env.plugin._allow_exec_host_cmd { + warn!("This plugin isn't allow to run command in host side, skip running this command: '{cmd} {args}'.", + cmd = command, args = cmdline.join(" ")); + return; + } + + // Here, we don't wait the command to finish + process::Command::new(command) + .args(cmdline) + .spawn() + .unwrap(); +} + +// Custom panic handler for plugins. +// +// This is called when a panic occurs in a plugin. Since most panics will likely originate in the +// code trying to deserialize an `Event` upon a plugin state update, we read some panic message, +// formatted as string from the plugin. +fn host_report_panic(plugin_env: &PluginEnv) { + let msg = wasi_read_string(&plugin_env.wasi_env); + panic!("{}", msg); +} + +// Helper Functions --------------------------------------------------------------------------------------------------- + +pub fn wasi_read_string(wasi_env: &WasiEnv) -> String { + let mut state = wasi_env.state(); + let wasi_file = state.fs.stdout_mut().unwrap().as_mut().unwrap(); + let mut buf = String::new(); + wasi_file.read_to_string(&mut buf).unwrap(); + // https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c + buf.replace("\n", "\n\r") +} + +pub fn wasi_write_string(wasi_env: &WasiEnv, buf: &str) { + let mut state = wasi_env.state(); + let wasi_file = state.fs.stdin_mut().unwrap().as_mut().unwrap(); + writeln!(wasi_file, "{}\r", buf).unwrap(); +} + +pub fn wasi_write_object(wasi_env: &WasiEnv, object: &(impl Serialize + ?Sized)) { + wasi_write_string(wasi_env, &serde_json::to_string(&object).unwrap()); +} + +pub fn wasi_read_object(wasi_env: &WasiEnv) -> T { + let json = wasi_read_string(wasi_env); + serde_json::from_str(&json).unwrap() +} diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index 5034e7a6..e81ef5b2 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -1,9 +1,9 @@ use crate::terminal_bytes::TerminalBytes; use crate::{ panes::PaneId, + plugins::PluginInstruction, screen::ScreenInstruction, thread_bus::{Bus, ThreadSenders}, - wasm_vm::PluginInstruction, ClientId, ServerInstruction, }; use async_std::task::{self, JoinHandle}; @@ -15,7 +15,7 @@ use zellij_utils::{ errors::{ContextType, PtyContext}, input::{ command::{RunCommand, TerminalAction}, - layout::{Layout, PaneLayout, Run}, + layout::{Layout, PaneLayout, Run, RunPluginLocation}, }, }; @@ -51,6 +51,8 @@ pub enum PtyInstruction { Option, Option, Option, + usize, // tab_index + HashMap>, // plugin_ids ClientId, ), // the String is the tab name ClosePane(PaneId), @@ -330,12 +332,21 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { format!("failed to move client {} to tab {}", client_id, tab_index) })?; }, - PtyInstruction::NewTab(terminal_action, tab_layout, tab_name, client_id) => { + PtyInstruction::NewTab( + terminal_action, + tab_layout, + tab_name, + tab_index, + plugin_ids, + client_id, + ) => { let err_context = || format!("failed to open new tab for client {}", client_id); pty.spawn_terminals_for_layout( tab_layout.unwrap_or_else(|| layout.new_tab()), terminal_action.clone(), + plugin_ids, + tab_index, client_id, ) .with_context(err_context)?; @@ -555,6 +566,8 @@ impl Pty { &mut self, layout: PaneLayout, default_shell: Option, + plugin_ids: HashMap>, + tab_index: usize, client_id: ClientId, ) -> Result<()> { let err_context = || format!("failed to spawn terminals for layout for client {client_id}"); @@ -747,9 +760,11 @@ impl Pty { .collect(); self.bus .senders - .send_to_screen(ScreenInstruction::NewTab( + .send_to_screen(ScreenInstruction::ApplyLayout( layout, new_tab_pane_ids, + plugin_ids, + tab_index, client_id, )) .with_context(err_context)?; diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 55e45085..74a876c2 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -2,9 +2,9 @@ use std::sync::{Arc, RwLock}; use crate::{ os_input_output::ServerOsApi, + plugins::PluginInstruction, pty::{ClientOrTabIndex, PtyInstruction}, screen::ScreenInstruction, - wasm_vm::PluginInstruction, ServerInstruction, SessionMetaData, SessionState, }; use zellij_utils::{ @@ -440,7 +440,7 @@ pub(crate) fn route_action( let shell = session.default_shell.clone(); session .senders - .send_to_pty(PtyInstruction::NewTab( + .send_to_screen(ScreenInstruction::NewTab( shell, tab_layout, tab_name, client_id, )) .with_context(err_context)?; diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 40bcc964..ea68e3e4 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -9,7 +9,11 @@ use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; use zellij_utils::input::options::Clipboard; use zellij_utils::pane_size::{Size, SizeInPixels}; -use zellij_utils::{input::command::TerminalAction, input::layout::PaneLayout, position::Position}; +use zellij_utils::{ + input::command::TerminalAction, + input::layout::{PaneLayout, RunPluginLocation}, + position::Position, +}; use crate::panes::alacritty_functions::xparse_color; use crate::panes::terminal_character::AnsiCode; @@ -18,11 +22,11 @@ use crate::{ output::Output, panes::sixel::SixelImageStore, panes::PaneId, + plugins::PluginInstruction, pty::{ClientOrTabIndex, PtyInstruction, VteBytes}, tab::Tab, thread_bus::Bus, ui::overlay::{Overlay, OverlayWindow, Overlayable}, - wasm_vm::PluginInstruction, ClientId, ServerInstruction, }; use zellij_utils::{ @@ -174,7 +178,19 @@ pub enum ScreenInstruction { HoldPane(PaneId, Option, RunCommand, Option), // Option is the exit status UpdatePaneName(Vec, ClientId), UndoRenamePane(ClientId), - NewTab(PaneLayout, Vec<(u32, HoldForCommand)>, ClientId), + NewTab( + Option, + Option, + Option, + ClientId, + ), + ApplyLayout( + PaneLayout, + Vec<(u32, HoldForCommand)>, + HashMap>, + usize, // tab_index + ClientId, + ), SwitchTabNext(ClientId), SwitchTabPrev(ClientId), ToggleActiveSyncTab(ClientId), @@ -275,6 +291,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::UpdatePaneName(..) => ScreenContext::UpdatePaneName, ScreenInstruction::UndoRenamePane(..) => ScreenContext::UndoRenamePane, ScreenInstruction::NewTab(..) => ScreenContext::NewTab, + ScreenInstruction::ApplyLayout(..) => ScreenContext::ApplyLayout, ScreenInstruction::SwitchTabNext(..) => ScreenContext::SwitchTabNext, ScreenInstruction::SwitchTabPrev(..) => ScreenContext::SwitchTabPrev, ScreenInstruction::CloseTab(..) => ScreenContext::CloseTab, @@ -761,7 +778,7 @@ impl Screen { let vte_overlay = overlay.generate_overlay(size).context(err_context)?; tab.render(&mut output, Some(vte_overlay)) .context(err_context)?; - } else { + } else if !tab.is_pending() { tabs_to_close.push(*tab_index); } } @@ -838,14 +855,8 @@ impl Screen { self.get_tabs_mut().get_mut(&tab_index) } - /// Creates a new [`Tab`] in this [`Screen`], applying the specified [`Layout`] - /// and switching to it. - pub fn new_tab( - &mut self, - layout: PaneLayout, - new_ids: Vec<(u32, HoldForCommand)>, - client_id: ClientId, - ) -> Result<()> { + /// Creates a new [`Tab`] in this [`Screen`] + pub fn new_tab(&mut self, tab_index: usize, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to create new tab for client {client_id:?}",); let client_id = if self.get_active_tab(client_id).is_ok() { @@ -856,9 +867,8 @@ impl Screen { client_id }; - let tab_index = self.get_new_tab_index(); let position = self.tabs.len(); - let mut tab = Tab::new( + let tab = Tab::new( tab_index, position, String::new(), @@ -882,35 +892,72 @@ impl Screen { self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), ); - tab.apply_layout(layout, new_ids, tab_index, client_id) - .with_context(err_context)?; - if self.session_is_mirrored { - if let Ok(active_tab) = self.get_active_tab_mut(client_id) { - let client_mode_infos_in_source_tab = active_tab.drain_connected_clients(None); - tab.add_multiple_clients(client_mode_infos_in_source_tab) - .with_context(err_context)?; - if active_tab.has_no_connected_clients() { - active_tab.visible(false).with_context(err_context)?; - } - } + self.tabs.insert(tab_index, tab); + Ok(()) + } + pub fn apply_layout( + &mut self, + layout: PaneLayout, + new_terminal_ids: Vec<(u32, HoldForCommand)>, + new_plugin_ids: HashMap>, + tab_index: usize, + client_id: ClientId, + ) -> Result<()> { + let client_id = if self.get_active_tab(client_id).is_ok() { + client_id + } else if let Some(first_client_id) = self.get_first_client_id() { + first_client_id + } else { + client_id + }; + let err_context = || format!("failed to apply layout for tab {tab_index:?}",); + + // move the relevant clients out of the current tab and place them in the new one + let drained_clients = if self.session_is_mirrored { + let client_mode_infos_in_source_tab = + if let Ok(active_tab) = self.get_active_tab_mut(client_id) { + let client_mode_infos_in_source_tab = active_tab.drain_connected_clients(None); + if active_tab.has_no_connected_clients() { + active_tab.visible(false).with_context(err_context)?; + } + Some(client_mode_infos_in_source_tab) + } else { + None + }; let all_connected_clients: Vec = self.connected_clients.borrow().iter().copied().collect(); for client_id in all_connected_clients { self.update_client_tab_focus(client_id, tab_index); } + client_mode_infos_in_source_tab } else if let Ok(active_tab) = self.get_active_tab_mut(client_id) { let client_mode_info_in_source_tab = active_tab.drain_connected_clients(Some(vec![client_id])); - tab.add_multiple_clients(client_mode_info_in_source_tab) - .with_context(err_context)?; if active_tab.has_no_connected_clients() { active_tab.visible(false).with_context(err_context)?; } self.update_client_tab_focus(client_id, tab_index); - } + Some(client_mode_info_in_source_tab) + } else { + None + }; + + // apply the layout to the new tab + let tab = self.tabs.get_mut(&tab_index).unwrap(); // TODO: no unwrap + tab.apply_layout( + layout, + new_terminal_ids, + new_plugin_ids, + tab_index, + client_id, + ) + .with_context(err_context)?; tab.update_input_modes().with_context(err_context)?; tab.visible(true).with_context(err_context)?; - self.tabs.insert(tab_index, tab); + if let Some(drained_clients) = drained_clients { + tab.add_multiple_clients(drained_clients) + .with_context(err_context)?; + } if !self.active_tab_indices.contains_key(&client_id) { // this means this is a new client and we need to add it to our state properly self.add_client(client_id).with_context(err_context)?; @@ -1853,8 +1900,28 @@ pub(crate) fn screen_thread_main( screen.unblock_input()?; screen.render()?; }, - ScreenInstruction::NewTab(layout, new_pane_pids, client_id) => { - screen.new_tab(layout, new_pane_pids, client_id)?; + ScreenInstruction::NewTab(default_shell, layout, tab_name, client_id) => { + let tab_index = screen.get_new_tab_index(); + screen.new_tab(tab_index, client_id)?; + screen + .bus + .senders + .send_to_plugin(PluginInstruction::NewTab( + default_shell, + layout, + tab_name, + tab_index, + client_id, + ))?; + }, + ScreenInstruction::ApplyLayout( + layout, + new_pane_pids, + new_plugin_ids, + tab_index, + client_id, + ) => { + screen.apply_layout(layout, new_pane_pids, new_plugin_ids, tab_index, client_id)?; screen.unblock_input()?; screen.render()?; }, diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 45200f7c..4d336988 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -23,15 +23,14 @@ use crate::{ panes::sixel::SixelImageStore, panes::{FloatingPanes, TiledPanes}, panes::{LinkHandler, PaneId, PluginPane, TerminalPane}, + plugins::PluginInstruction, pty::{ClientOrTabIndex, PtyInstruction, VteBytes}, thread_bus::ThreadSenders, - wasm_vm::PluginInstruction, ClientId, ServerInstruction, }; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::rc::Rc; -use std::sync::mpsc::channel; use std::time::Instant; use std::{ collections::{HashMap, HashSet}, @@ -41,7 +40,7 @@ use zellij_utils::{ data::{Event, InputMode, ModeInfo, Palette, PaletteColor, Style}, input::{ command::TerminalAction, - layout::{PaneLayout, Run}, + layout::{PaneLayout, Run, RunPluginLocation}, parse_keys, }, pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport}, @@ -70,14 +69,19 @@ macro_rules! resize_pty { }}; } -type HoldForCommand = Option; - // FIXME: This should be replaced by `RESIZE_PERCENT` at some point pub const MIN_TERMINAL_HEIGHT: usize = 5; pub const MIN_TERMINAL_WIDTH: usize = 5; const MAX_PENDING_VTE_EVENTS: usize = 7000; +type HoldForCommand = Option; + +enum BufferedTabInstruction { + SetPaneSelectable(PaneId, bool), + HandlePtyBytes(u32, VteBytes), +} + pub(crate) struct Tab { pub index: usize, pub position: usize, @@ -113,8 +117,11 @@ pub(crate) struct Tab { terminal_emulator_color_codes: Rc>>, pids_waiting_resize: HashSet, // u32 is the terminal_id cursor_positions_and_shape: HashMap, // (x_position, - // y_position, - // cursor_shape_csi) + // y_position, + // cursor_shape_csi) + is_pending: bool, // a pending tab is one that is still being loaded or otherwise waiting + pending_instructions: Vec, // instructions that came while the tab was + // pending and need to be re-applied } #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -489,13 +496,16 @@ impl Tab { terminal_emulator_color_codes, pids_waiting_resize: HashSet::new(), cursor_positions_and_shape: HashMap::new(), + is_pending: true, // will be switched to false once the layout is applied + pending_instructions: vec![], } } pub fn apply_layout( &mut self, layout: PaneLayout, - new_ids: Vec<(u32, HoldForCommand)>, + new_terminal_ids: Vec<(u32, HoldForCommand)>, + mut new_plugin_ids: HashMap>, tab_index: usize, client_id: ClientId, ) -> Result<()> { @@ -523,7 +533,7 @@ impl Tab { match layout.position_panes_in_space(&free_space) { Ok(positions_in_layout) => { let positions_and_size = positions_in_layout.iter(); - let mut new_ids = new_ids.iter(); + let mut new_terminal_ids = new_terminal_ids.iter(); let mut focus_pane_id: Option = None; let mut set_focus_pane_id = |layout: &PaneLayout, pane_id: PaneId| { @@ -535,18 +545,16 @@ impl Tab { for (layout, position_and_size) in positions_and_size { // A plugin pane if let Some(Run::Plugin(run)) = layout.run.clone() { - let (pid_tx, pid_rx) = channel(); + // let (pid_tx, pid_rx) = channel(); let pane_title = run.location.to_string(); - self.senders - .send_to_plugin(PluginInstruction::Load( - pid_tx, - run, - tab_index, - client_id, - position_and_size.into(), - )) - .with_context(err_context)?; - let pid = pid_rx.recv().with_context(err_context)?; + let pid = new_plugin_ids + .get_mut(&run.location) + .unwrap() + .pop() + .unwrap(); // TODO: + // err_context + // and + // stuff let mut new_plugin = PluginPane::new( pid, *position_and_size, @@ -570,7 +578,7 @@ impl Tab { set_focus_pane_id(layout, PaneId::Plugin(pid)); } else { // there are still panes left to fill, use the pids we received in this method - if let Some((pid, hold_for_command)) = new_ids.next() { + if let Some((pid, hold_for_command)) = new_terminal_ids.next() { let next_terminal_position = self.get_next_terminal_position(); let initial_title = match &layout.run { Some(Run::Command(run_command)) => Some(run_command.to_string()), @@ -601,7 +609,7 @@ impl Tab { } } } - for (unused_pid, _) in new_ids { + for (unused_pid, _) in new_terminal_ids { // this is a bit of a hack and happens because we don't have any central location that // can query the screen as to how many panes it needs to create a layout // fixing this will require a bit of an architecture change @@ -639,14 +647,17 @@ impl Tab { }, } } + self.is_pending = false; + self.apply_buffered_instructions()?; Ok(()) }, Err(e) => { - for (unused_pid, _) in new_ids { + for (unused_pid, _) in new_terminal_ids { self.senders .send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(unused_pid))) .with_context(err_context)?; } + self.is_pending = false; Err::<(), _>(anyError::msg(e)) .with_context(err_context) .non_fatal(); // TODO: propagate this to the user @@ -654,6 +665,21 @@ impl Tab { }, } } + pub fn apply_buffered_instructions(&mut self) -> Result<()> { + let buffered_instructions: Vec = + self.pending_instructions.drain(..).collect(); + for buffered_instruction in buffered_instructions { + match buffered_instruction { + BufferedTabInstruction::SetPaneSelectable(pane_id, selectable) => { + self.set_pane_selectable(pane_id, selectable); + }, + BufferedTabInstruction::HandlePtyBytes(terminal_id, bytes) => { + self.handle_pty_bytes(terminal_id, bytes)?; + }, + } + } + Ok(()) + } pub fn update_input_modes(&mut self) -> Result<()> { // this updates all plugins with the client's input mode let mode_infos = self.mode_info.borrow(); @@ -1120,6 +1146,11 @@ impl Tab { .any(|s_p| s_p.pid() == PaneId::Plugin(plugin_id)) } pub fn handle_pty_bytes(&mut self, pid: u32, bytes: VteBytes) -> Result<()> { + if self.is_pending { + self.pending_instructions + .push(BufferedTabInstruction::HandlePtyBytes(pid, bytes)); + return Ok(()); + } let err_context = || format!("failed to handle pty bytes from fd {pid}"); if let Some(terminal_output) = self .tiled_panes @@ -1818,6 +1849,11 @@ impl Tab { .collect() } pub fn set_pane_selectable(&mut self, id: PaneId, selectable: bool) { + if self.is_pending { + self.pending_instructions + .push(BufferedTabInstruction::SetPaneSelectable(id, selectable)); + return; + } if let Some(pane) = self.tiled_panes.get_pane_mut(id) { pane.set_selectable(selectable); if !selectable { @@ -2807,6 +2843,10 @@ impl Tab { } } + pub fn is_pending(&self) -> bool { + self.is_pending + } + fn show_floating_panes(&mut self) { // this function is to be preferred to directly invoking floating_panes.toggle_show_panes(true) self.floating_panes.toggle_show_panes(true); diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index a95452ac..c635b125 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -224,8 +224,14 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab { terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) - .unwrap(); + tab.apply_layout( + PaneLayout::default(), + vec![(1, None)], + HashMap::new(), + index, + client_id, + ) + .unwrap(); tab } @@ -274,8 +280,14 @@ fn create_new_tab_with_os_api( terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) - .unwrap(); + tab.apply_layout( + PaneLayout::default(), + vec![(1, None)], + HashMap::new(), + index, + client_id, + ) + .unwrap(); tab } @@ -328,7 +340,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str) .enumerate() .map(|(i, _)| (i as u32, None)) .collect(); - tab.apply_layout(tab_layout, pane_ids, index, client_id) + tab.apply_layout(tab_layout, pane_ids, HashMap::new(), index, client_id) .unwrap(); tab } @@ -379,8 +391,14 @@ fn create_new_tab_with_mock_pty_writer( terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) - .unwrap(); + tab.apply_layout( + PaneLayout::default(), + vec![(1, None)], + HashMap::new(), + index, + client_id, + ) + .unwrap(); tab } @@ -432,8 +450,14 @@ fn create_new_tab_with_sixel_support( terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) - .unwrap(); + tab.apply_layout( + PaneLayout::default(), + vec![(1, None)], + HashMap::new(), + index, + client_id, + ) + .unwrap(); tab } diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index 8a0d59ed..4ddd0fda 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -138,8 +138,14 @@ fn create_new_tab(size: Size) -> Tab { terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) - .unwrap(); + tab.apply_layout( + PaneLayout::default(), + vec![(1, None)], + HashMap::new(), + index, + client_id, + ) + .unwrap(); tab } @@ -185,8 +191,14 @@ fn create_new_tab_with_cell_size( terminal_emulator_colors, terminal_emulator_color_codes, ); - tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id) - .unwrap(); + tab.apply_layout( + PaneLayout::default(), + vec![(1, None)], + HashMap::new(), + index, + client_id, + ) + .unwrap(); tab } diff --git a/zellij-server/src/thread_bus.rs b/zellij-server/src/thread_bus.rs index 28c3082e..8d172242 100644 --- a/zellij-server/src/thread_bus.rs +++ b/zellij-server/src/thread_bus.rs @@ -1,8 +1,8 @@ //! Definitions and helpers for sending and receiving messages between threads. use crate::{ - os_input_output::ServerOsApi, pty::PtyInstruction, pty_writer::PtyWriteInstruction, - screen::ScreenInstruction, wasm_vm::PluginInstruction, ServerInstruction, + os_input_output::ServerOsApi, plugins::PluginInstruction, pty::PtyInstruction, + pty_writer::PtyWriteInstruction, screen::ScreenInstruction, ServerInstruction, }; use zellij_utils::errors::prelude::*; use zellij_utils::{channels, channels::SenderWithContext, errors::ErrorContext}; diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 240a3464..89e8b386 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -23,7 +23,7 @@ use std::env::set_var; use std::os::unix::io::RawFd; use std::sync::{Arc, Mutex}; -use crate::{pty::PtyInstruction, wasm_vm::PluginInstruction}; +use crate::{plugins::PluginInstruction, pty::PtyInstruction}; use zellij_utils::ipc::PixelDimensions; use zellij_utils::{ @@ -221,7 +221,7 @@ fn create_new_screen(size: Size) -> Screen { let session_is_mirrored = true; let copy_options = CopyOptions::default(); - Screen::new( + let screen = Screen::new( bus, &client_attributes, max_panes, @@ -229,7 +229,8 @@ fn create_new_screen(size: Size) -> Screen { draw_pane_frames, session_is_mirrored, copy_options, - ) + ); + screen } struct MockScreen { @@ -248,6 +249,7 @@ struct MockScreen { pub client_attributes: ClientAttributes, pub config_options: Options, pub session_metadata: SessionMetaData, + last_opened_tab_index: Option, } impl MockScreen { @@ -280,27 +282,53 @@ impl MockScreen { let pane_layout = initial_layout.unwrap_or_default(); let pane_count = pane_layout.extract_run_instructions().len(); let mut pane_ids = vec![]; + let plugin_ids = HashMap::new(); for i in 0..pane_count { pane_ids.push((i as u32, None)); } + let default_shell = None; + let tab_name = None; + let tab_index = self.last_opened_tab_index.map(|l| l + 1).unwrap_or(0); let _ = self.to_screen.send(ScreenInstruction::NewTab( - pane_layout, - pane_ids, + default_shell, + Some(pane_layout.clone()), + tab_name, self.main_client_id, )); + let _ = self.to_screen.send(ScreenInstruction::ApplyLayout( + pane_layout, + pane_ids, + plugin_ids, + tab_index, + self.main_client_id, + )); + self.last_opened_tab_index = Some(tab_index); screen_thread } pub fn new_tab(&mut self, tab_layout: PaneLayout) { let pane_count = tab_layout.extract_run_instructions().len(); let mut pane_ids = vec![]; + let plugin_ids = HashMap::new(); + let default_shell = None; + let tab_name = None; + let tab_index = self.last_opened_tab_index.map(|l| l + 1).unwrap_or(0); for i in 0..pane_count { pane_ids.push((i as u32, None)); } let _ = self.to_screen.send(ScreenInstruction::NewTab( - tab_layout, - pane_ids, + default_shell, + Some(tab_layout.clone()), + tab_name, self.main_client_id, )); + let _ = self.to_screen.send(ScreenInstruction::ApplyLayout( + tab_layout, + pane_ids, + plugin_ids, + 0, + self.main_client_id, + )); + self.last_opened_tab_index = Some(tab_index); } pub fn teardown(&mut self, threads: Vec>) { let _ = self.to_pty.send(PtyInstruction::Exit); @@ -321,7 +349,7 @@ impl MockScreen { default_shell: self.session_metadata.default_shell.clone(), screen_thread: None, pty_thread: None, - wasm_thread: None, + plugin_thread: None, pty_writer_thread: None, } } @@ -369,7 +397,7 @@ impl MockScreen { client_attributes: client_attributes.clone(), screen_thread: None, pty_thread: None, - wasm_thread: None, + plugin_thread: None, pty_writer_thread: None, }; @@ -392,6 +420,7 @@ impl MockScreen { client_attributes, config_options, session_metadata, + last_opened_tab_index: None, } } } @@ -421,10 +450,19 @@ macro_rules! log_actions_in_thread { }; } -fn new_tab(screen: &mut Screen, pid: u32) { +fn new_tab(screen: &mut Screen, pid: u32, tab_index: usize) { let client_id = 1; + let new_terminal_ids = vec![(pid, None)]; + let new_plugin_ids = HashMap::new(); + screen.new_tab(tab_index, client_id).expect("TEST"); screen - .new_tab(PaneLayout::default(), vec![(pid, None)], client_id) + .apply_layout( + PaneLayout::default(), + new_terminal_ids, + new_plugin_ids, + tab_index, + client_id, + ) .expect("TEST"); } @@ -436,8 +474,8 @@ fn open_new_tab() { }; let mut screen = create_new_screen(size); - new_tab(&mut screen, 1); - new_tab(&mut screen, 2); + new_tab(&mut screen, 1, 0); + new_tab(&mut screen, 2, 1); assert_eq!(screen.tabs.len(), 2, "Screen now has two tabs"); assert_eq!( @@ -455,8 +493,8 @@ pub fn switch_to_prev_tab() { }; let mut screen = create_new_screen(size); - new_tab(&mut screen, 1); - new_tab(&mut screen, 2); + new_tab(&mut screen, 1, 1); + new_tab(&mut screen, 2, 2); screen.switch_tab_prev(1).expect("TEST"); assert_eq!( @@ -474,8 +512,8 @@ pub fn switch_to_next_tab() { }; let mut screen = create_new_screen(size); - new_tab(&mut screen, 1); - new_tab(&mut screen, 2); + new_tab(&mut screen, 1, 1); + new_tab(&mut screen, 2, 2); screen.switch_tab_prev(1).expect("TEST"); screen.switch_tab_next(1).expect("TEST"); @@ -494,8 +532,8 @@ pub fn close_tab() { }; let mut screen = create_new_screen(size); - new_tab(&mut screen, 1); - new_tab(&mut screen, 2); + new_tab(&mut screen, 1, 1); + new_tab(&mut screen, 2, 2); screen.close_tab(1).expect("TEST"); assert_eq!(screen.tabs.len(), 1, "Only one tab left"); @@ -514,9 +552,9 @@ pub fn close_the_middle_tab() { }; let mut screen = create_new_screen(size); - new_tab(&mut screen, 1); - new_tab(&mut screen, 2); - new_tab(&mut screen, 3); + new_tab(&mut screen, 1, 1); + new_tab(&mut screen, 2, 2); + new_tab(&mut screen, 3, 3); screen.switch_tab_prev(1).expect("TEST"); screen.close_tab(1).expect("TEST"); @@ -536,9 +574,9 @@ fn move_focus_left_at_left_screen_edge_changes_tab() { }; let mut screen = create_new_screen(size); - new_tab(&mut screen, 1); - new_tab(&mut screen, 2); - new_tab(&mut screen, 3); + new_tab(&mut screen, 1, 1); + new_tab(&mut screen, 2, 2); + new_tab(&mut screen, 3, 3); screen.switch_tab_prev(1).expect("TEST"); screen.move_focus_left_or_previous_tab(1).expect("TEST"); @@ -557,9 +595,9 @@ fn move_focus_right_at_right_screen_edge_changes_tab() { }; let mut screen = create_new_screen(size); - new_tab(&mut screen, 1); - new_tab(&mut screen, 2); - new_tab(&mut screen, 3); + new_tab(&mut screen, 1, 1); + new_tab(&mut screen, 2, 2); + new_tab(&mut screen, 3, 3); screen.switch_tab_prev(1).expect("TEST"); screen.move_focus_right_or_next_tab(1).expect("TEST"); @@ -578,8 +616,8 @@ pub fn toggle_to_previous_tab_simple() { }; let mut screen = create_new_screen(position_and_size); - new_tab(&mut screen, 1); - new_tab(&mut screen, 2); + new_tab(&mut screen, 1, 1); + new_tab(&mut screen, 2, 2); screen.go_to_tab(1, 1).expect("TEST"); screen.go_to_tab(2, 1).expect("TEST"); @@ -606,9 +644,9 @@ pub fn toggle_to_previous_tab_create_tabs_only() { }; let mut screen = create_new_screen(position_and_size); - new_tab(&mut screen, 1); - new_tab(&mut screen, 2); - new_tab(&mut screen, 3); + new_tab(&mut screen, 1, 0); + new_tab(&mut screen, 2, 1); + new_tab(&mut screen, 3, 2); assert_eq!( screen.tab_history.get(&1).unwrap(), @@ -656,10 +694,10 @@ pub fn toggle_to_previous_tab_delete() { }; let mut screen = create_new_screen(position_and_size); - new_tab(&mut screen, 1); // 0 - new_tab(&mut screen, 2); // 1 - new_tab(&mut screen, 3); // 2 - new_tab(&mut screen, 4); // 3 + new_tab(&mut screen, 1, 0); + new_tab(&mut screen, 2, 1); + new_tab(&mut screen, 3, 2); + new_tab(&mut screen, 4, 3); assert_eq!( screen.tab_history.get(&1).unwrap(), @@ -752,7 +790,7 @@ fn switch_to_tab_with_fullscreen() { }; let mut screen = create_new_screen(size); - new_tab(&mut screen, 1); + new_tab(&mut screen, 1, 1); { let active_tab = screen.get_active_tab_mut(1).unwrap(); active_tab @@ -760,7 +798,7 @@ fn switch_to_tab_with_fullscreen() { .unwrap(); active_tab.toggle_active_pane_fullscreen(1); } - new_tab(&mut screen, 2); + new_tab(&mut screen, 2, 2); screen.switch_tab_prev(1).expect("TEST"); @@ -867,7 +905,7 @@ fn attach_after_first_tab_closed() { }; let mut screen = create_new_screen(size); - new_tab(&mut screen, 1); + new_tab(&mut screen, 1, 0); { let active_tab = screen.get_active_tab_mut(1).unwrap(); active_tab @@ -875,7 +913,7 @@ fn attach_after_first_tab_closed() { .unwrap(); active_tab.toggle_active_pane_fullscreen(1); } - new_tab(&mut screen, 2); + new_tab(&mut screen, 2, 1); screen.close_tab_at_index(0).expect("TEST"); screen.remove_client(1).expect("TEST"); @@ -2208,12 +2246,12 @@ pub fn send_cli_new_tab_action_default_params() { let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); let screen_thread = mock_screen.run(Some(initial_layout)); - let received_pty_instructions = Arc::new(Mutex::new(vec![])); - let pty_receiver = mock_screen.pty_receiver.take().unwrap(); - let pty_thread = log_actions_in_thread!( - received_pty_instructions, - PtyInstruction::Exit, - pty_receiver + let received_plugin_instructions = Arc::new(Mutex::new(vec![])); + let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); + let plugin_thread = log_actions_in_thread!( + received_plugin_instructions, + PluginInstruction::Exit, + plugin_receiver ); let new_tab_action = CliAction::NewTab { name: None, @@ -2227,8 +2265,16 @@ pub fn send_cli_new_tab_action_default_params() { client_id, ); std::thread::sleep(std::time::Duration::from_millis(100)); - mock_screen.teardown(vec![pty_thread, screen_thread]); - assert_snapshot!(format!("{:?}", *received_pty_instructions.lock().unwrap())); + mock_screen.teardown(vec![plugin_thread, screen_thread]); + let received_plugin_instructions = received_plugin_instructions.lock().unwrap(); + let new_tab_action = + received_plugin_instructions + .iter() + .find(|instruction| match instruction { + PluginInstruction::NewTab(..) => true, + _ => false, + }); + assert_snapshot!(format!("{:#?}", new_tab_action)); } #[test] @@ -2241,12 +2287,12 @@ pub fn send_cli_new_tab_action_with_name_and_layout() { let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); let screen_thread = mock_screen.run(Some(initial_layout)); - let received_pty_instructions = Arc::new(Mutex::new(vec![])); - let pty_receiver = mock_screen.pty_receiver.take().unwrap(); - let pty_thread = log_actions_in_thread!( - received_pty_instructions, - PtyInstruction::Exit, - pty_receiver + let received_plugin_instructions = Arc::new(Mutex::new(vec![])); + let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); + let plugin_thread = log_actions_in_thread!( + received_plugin_instructions, + PluginInstruction::Exit, + plugin_receiver ); let new_tab_action = CliAction::NewTab { name: Some("my-awesome-tab-name".into()), @@ -2263,13 +2309,14 @@ pub fn send_cli_new_tab_action_with_name_and_layout() { client_id, ); std::thread::sleep(std::time::Duration::from_millis(100)); - mock_screen.teardown(vec![pty_thread, screen_thread]); - let new_tab_instruction = received_pty_instructions + mock_screen.teardown(vec![plugin_thread, screen_thread]); + let new_tab_instruction = received_plugin_instructions .lock() .unwrap() .iter() + .rev() .find(|i| { - if let PtyInstruction::NewTab(..) = i { + if let PluginInstruction::NewTab(..) = i { return true; } else { return false; diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap index 53a02426..51737530 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap @@ -1,6 +1,46 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 1898 -expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" +assertion_line: 2272 +expression: "format!(\"{:#?}\", new_tab_action)" --- -[NewTab(None, None, None, 10), UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), Exit] +Some( + NewTab( + None, + Some( + PaneLayout { + children_split_direction: Vertical, + name: None, + children: [ + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ), + None, + 0, + 1, + ), +) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap index aac07353..b2f34a98 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 2287 +assertion_line: 2322 expression: "format!(\"{:#?}\", new_tab_instruction)" --- NewTab( @@ -63,5 +63,6 @@ NewTab( Some( "my-awesome-tab-name", ), + 1, 10, ) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap index 40b651ae..dc74bc3c 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 2528 +assertion_line: 2506 expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())" --- [ @@ -18,6 +18,45 @@ expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())" ), InputReceived, ), + NewTab( + None, + Some( + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [ + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ), + None, + 0, + 1, + ), Update( None, Some( @@ -190,6 +229,45 @@ expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())" ], ), ), + NewTab( + None, + Some( + PaneLayout { + children_split_direction: Vertical, + name: None, + children: [ + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ), + None, + 1, + 1, + ), Update( None, Some( diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap index dbe2c85a..2ee0a77e 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 2571 +assertion_line: 2549 expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())" --- [ @@ -18,6 +18,45 @@ expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())" ), InputReceived, ), + NewTab( + None, + Some( + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [ + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ), + None, + 0, + 1, + ), Update( None, Some( @@ -190,6 +229,45 @@ expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())" ], ), ), + NewTab( + None, + Some( + PaneLayout { + children_split_direction: Vertical, + name: None, + children: [ + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + PaneLayout { + children_split_direction: Horizontal, + name: None, + children: [], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ], + split_size: None, + run: None, + borderless: false, + focus: None, + external_children_index: None, + }, + ), + None, + 1, + 1, + ), Update( None, Some( diff --git a/zellij-server/src/wasm_vm.rs b/zellij-server/src/wasm_vm.rs deleted file mode 100644 index 8bc55ab1..00000000 --- a/zellij-server/src/wasm_vm.rs +++ /dev/null @@ -1,785 +0,0 @@ -use highway::{HighwayHash, PortableHash}; -use log::{debug, info, warn}; -use semver::Version; -use serde::{de::DeserializeOwned, Serialize}; -use std::{ - collections::{HashMap, HashSet}, - fmt, fs, - path::{Path, PathBuf}, - process, - str::FromStr, - sync::{mpsc::Sender, Arc, Mutex}, - 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 crate::{ - logging_pipe::LoggingPipe, - panes::PaneId, - pty::{ClientOrTabIndex, PtyInstruction}, - screen::ScreenInstruction, - thread_bus::{Bus, ThreadSenders}, - ClientId, -}; - -use zellij_utils::{ - consts::{DEBUG_MODE, VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR}, - data::{Event, EventType, PluginIds}, - errors::{prelude::*, ContextType, PluginContext}, - input::{ - command::TerminalAction, - layout::RunPlugin, - plugins::{PluginConfig, PluginType, PluginsConfig}, - }, - pane_size::Size, - serde, -}; - -/// Custom error for plugin version mismatch. -/// -/// This is thrown when, during starting a plugin, it is detected that the plugin version doesn't -/// match the zellij version. This is treated as a fatal error and leads to instantaneous -/// termination. -#[derive(Debug)] -pub struct VersionMismatchError { - zellij_version: String, - plugin_version: String, - plugin_path: PathBuf, - // true for builtin plugins - builtin: bool, -} - -impl std::error::Error for VersionMismatchError {} - -impl VersionMismatchError { - pub fn new( - zellij_version: &str, - plugin_version: &str, - plugin_path: &PathBuf, - builtin: bool, - ) -> Self { - VersionMismatchError { - zellij_version: zellij_version.to_owned(), - plugin_version: plugin_version.to_owned(), - plugin_path: plugin_path.to_owned(), - builtin, - } - } -} - -impl fmt::Display for VersionMismatchError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let first_line = if self.builtin { - "It seems your version of zellij was built with outdated core plugins." - } else { - "If you're seeing this error a plugin version doesn't match the current -zellij version." - }; - - write!( - f, - "{} -Detected versions: - -- Plugin version: {} -- Zellij version: {} -- Offending plugin: {} - -If you're a user: - Please contact the distributor of your zellij version and report this error - to them. - -If you're a developer: - Please run zellij with updated plugins. The easiest way to achieve this - is to build zellij with `cargo make 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() - ) - } -} - -#[derive(Clone, Debug)] -pub enum PluginInstruction { - Load(Sender, RunPlugin, usize, ClientId, Size), // tx_pid, plugin metadata, tab_index, client_ids - Update(Option, Option, Event), // Focused plugin / broadcast, client_id, event data - Unload(u32), // plugin_id - Resize(u32, usize, usize), // plugin_id, columns, rows - AddClient(ClientId), - RemoveClient(ClientId), - Exit, -} - -impl From<&PluginInstruction> for PluginContext { - fn from(plugin_instruction: &PluginInstruction) -> Self { - match *plugin_instruction { - PluginInstruction::Load(..) => PluginContext::Load, - PluginInstruction::Update(..) => PluginContext::Update, - PluginInstruction::Unload(..) => PluginContext::Unload, - PluginInstruction::Resize(..) => PluginContext::Resize, - PluginInstruction::Exit => PluginContext::Exit, - PluginInstruction::AddClient(_) => PluginContext::AddClient, - PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient, - } - } -} - -#[derive(WasmerEnv, Clone)] -pub(crate) struct PluginEnv { - pub plugin_id: u32, - pub plugin: PluginConfig, - pub senders: ThreadSenders, - pub wasi_env: WasiEnv, - pub subscriptions: Arc>>, - pub tab_index: usize, - pub client_id: ClientId, - #[allow(dead_code)] - plugin_own_data_dir: PathBuf, -} - -// Thread main -------------------------------------------------------------------------------------------------------- -pub(crate) fn wasm_thread_main( - bus: Bus, - store: Store, - data_dir: PathBuf, - plugins: PluginsConfig, -) -> Result<()> { - info!("Wasm main thread starts"); - - let mut plugin_id = 0; - let mut headless_plugins = HashMap::new(); - let mut plugin_map: HashMap<(u32, ClientId), (Instance, PluginEnv, (usize, usize))> = - HashMap::new(); // u32 => pid, (usize, usize) => rows/columns TODO: clean this up into a struct or something - let mut connected_clients: Vec = vec![]; - let plugin_dir = data_dir.join("plugins/"); - let plugin_global_data_dir = plugin_dir.join("data"); - // Caches the "wasm bytes" of all plugins that have been loaded since zellij was started. - // Greatly decreases loading times of all plugins and avoids accesses to the hard-drive during - // "regular" operation. - let mut plugin_cache: HashMap = HashMap::new(); - - loop { - 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(pid_tx, run, tab_index, client_id, size) => { - let err_context = || format!("failed to load plugin for client {client_id}"); - - let plugin = plugins - .get(&run) - .with_context(|| format!("failed to resolve plugin {run:?}")) - .with_context(err_context) - .fatal(); - - let (instance, plugin_env) = start_plugin( - plugin_id, - client_id, - &plugin, - tab_index, - &bus, - &store, - &plugin_dir, - &mut plugin_cache, - ) - .with_context(err_context)?; - - let mut main_user_instance = instance.clone(); - let main_user_env = plugin_env.clone(); - load_plugin(&mut main_user_instance).with_context(err_context)?; - - plugin_map.insert( - (plugin_id, client_id), - (main_user_instance, main_user_env, (size.rows, size.cols)), - ); - - // clone plugins for the rest of the client ids if they exist - for client_id in 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(&store, &new_plugin_env); - let mut instance = Instance::new(&module, &zellij.chain_back(wasi)) - .with_context(err_context)?; - load_plugin(&mut instance).with_context(err_context)?; - plugin_map.insert( - (plugin_id, *client_id), - (instance, new_plugin_env, (size.rows, size.cols)), - ); - } - pid_tx.send(plugin_id).with_context(err_context)?; - plugin_id += 1; - }, - PluginInstruction::Update(pid, cid, event) => { - let err_context = || { - if *DEBUG_MODE.get().unwrap_or(&true) { - format!("failed to update plugin state with event: {event:#?}") - } else { - "failed to update plugin state".to_string() - } - }; - - for (&(plugin_id, client_id), (instance, plugin_env, (rows, columns))) in - &plugin_map - { - let subs = plugin_env - .subscriptions - .lock() - .to_anyhow() - .with_context(err_context)?; - // FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType? - let event_type = - EventType::from_str(&event.to_string()).with_context(err_context)?; - if subs.contains(&event_type) - && ((pid.is_none() && cid.is_none()) - || (pid.is_none() && cid == Some(client_id)) - || (cid.is_none() && pid == Some(plugin_id)) - || (cid == Some(client_id) && pid == Some(plugin_id))) - { - let update = instance - .exports - .get_function("update") - .with_context(err_context)?; - wasi_write_object(&plugin_env.wasi_env, &event); - 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 render = instance - .exports - .get_function("render") - .with_context(err_context)?; - render - .call(&[Value::I32(*rows as i32), Value::I32(*columns as i32)]) - .with_context(err_context)?; - let rendered_bytes = wasi_read_string(&plugin_env.wasi_env); - drop(bus.senders.send_to_screen(ScreenInstruction::PluginBytes( - plugin_id, - client_id, - rendered_bytes.as_bytes().to_vec(), - ))); - } - } - } - }, - PluginInstruction::Unload(pid) => { - info!("Bye from plugin {}", &pid); - // TODO: remove plugin's own data directory - 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(plugin_map.remove(&(plugin_id, client_id))); - } - } - }, - PluginInstruction::Resize(pid, new_columns, new_rows) => { - let err_context = || format!("failed to resize plugin {pid}"); - for ( - (plugin_id, client_id), - (instance, plugin_env, (current_rows, current_columns)), - ) in plugin_map.iter_mut() - { - if *plugin_id == pid { - *current_rows = new_rows; - *current_columns = new_columns; - - // TODO: consolidate with above render function - let render = instance - .exports - .get_function("render") - .with_context(err_context)?; - - render - .call(&[ - Value::I32(*current_rows as i32), - Value::I32(*current_columns as i32), - ]) - .with_context(err_context)?; - let rendered_bytes = wasi_read_string(&plugin_env.wasi_env); - drop(bus.senders.send_to_screen(ScreenInstruction::PluginBytes( - *plugin_id, - *client_id, - rendered_bytes.as_bytes().to_vec(), - ))); - } - } - }, - PluginInstruction::AddClient(client_id) => { - let err_context = || format!("failed to add plugins for client {client_id}"); - - connected_clients.push(client_id); - - let mut seen = HashSet::new(); - let mut new_plugins = HashMap::new(); - for (&(plugin_id, _), (instance, plugin_env, (rows, columns))) in &plugin_map { - if seen.contains(&plugin_id) { - continue; - } - seen.insert(plugin_id); - let mut new_plugin_env = plugin_env.clone(); - - new_plugin_env.client_id = client_id; - new_plugins.insert( - plugin_id, - (instance.module().clone(), new_plugin_env, (*rows, *columns)), - ); - } - for (plugin_id, (module, mut new_plugin_env, (rows, columns))) in - new_plugins.drain() - { - 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(&mut instance).with_context(err_context)?; - plugin_map.insert( - (plugin_id, client_id), - (instance, new_plugin_env, (rows, columns)), - ); - } - - // load headless plugins - for plugin in plugins.iter() { - if let PluginType::Headless = plugin.run { - let (instance, plugin_env) = start_plugin( - plugin_id, - client_id, - plugin, - 0, - &bus, - &store, - &plugin_dir, - &mut plugin_cache, - ) - .with_context(err_context)?; - headless_plugins.insert(plugin_id, (instance, plugin_env)); - plugin_id += 1; - } - } - }, - PluginInstruction::RemoveClient(client_id) => { - connected_clients.retain(|c| c != &client_id); - }, - PluginInstruction::Exit => break, - } - } - info!("wasm main thread exits"); - - fs::remove_dir_all(&plugin_global_data_dir) - .or_else(|err| { - if err.kind() == std::io::ErrorKind::NotFound { - // I don't care... - Ok(()) - } else { - Err(err) - } - }) - .context("failed to cleanup plugin data directory") -} - -#[allow(clippy::too_many_arguments)] -fn start_plugin( - plugin_id: u32, - client_id: ClientId, - plugin: &PluginConfig, - tab_index: usize, - bus: &Bus, - store: &Store, - plugin_dir: &Path, - plugin_cache: &mut HashMap, -) -> 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 = plugin_cache.contains_key(&plugin.path); - - // 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 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(plugin_dir) - .with_context(err_context) - .fatal(); - - fs::create_dir_all(&plugin_own_data_dir) - .with_context(|| format!("failed to create datadir in {plugin_own_data_dir:?}")) - .with_context(err_context) - .non_fatal(); - - // ensure tmp dir exists, in case it somehow was deleted (e.g systemd-tmpfiles) - 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) - .non_fatal(); - - let hash: String = PortableHash::default() - .hash256(&wasm_bytes) - .iter() - .map(ToString::to_string) - .collect(); - let cached_path = ZELLIJ_CACHE_DIR.join(&hash); - - unsafe { - match Module::deserialize_from_file(store, &cached_path) { - Ok(m) => { - log::debug!( - "Loaded plugin '{}' from cache folder at '{}'", - plugin.path.display(), - ZELLIJ_CACHE_DIR.display(), - ); - m - }, - Err(e) => { - let inner_context = || format!("failed to recover from {e:?}"); - - let m = Module::new(store, &wasm_bytes) - .with_context(inner_context) - .with_context(err_context)?; - fs::create_dir_all(ZELLIJ_CACHE_DIR.to_owned()) - .with_context(inner_context) - .with_context(err_context)?; - m.serialize_to_file(&cached_path) - .with_context(inner_context) - .with_context(err_context)?; - m - }, - } - } - }, - }; - - 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: bus.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)?; - - 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(); - plugin_cache.insert(cloned_plugin.path, module); - - Ok((instance, plugin_env)) -} - -// 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(), - ))) - }, - }; - plugin_version_func.call(&[]).with_context(err_context)?; - let plugin_version_str = wasi_read_string(&plugin_env.wasi_env); - let plugin_version = Version::parse(&plugin_version_str) - .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_str, - &plugin_env.plugin.path, - plugin_env.plugin.is_builtin(), - ))); - } - - Ok(()) -} - -fn load_plugin(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(()) -} - -// Plugin API --------------------------------------------------------------------------------------------------------- - -pub(crate) fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObject { - macro_rules! zellij_export { - ($($host_function:ident),+ $(,)?) => { - imports! { - "zellij" => { - $(stringify!($host_function) => - Function::new_native_with_env(store, plugin_env.clone(), $host_function),)+ - } - } - } - } - - zellij_export! { - host_subscribe, - host_unsubscribe, - host_set_selectable, - host_get_plugin_ids, - host_get_zellij_version, - host_open_file, - host_switch_tab_to, - host_set_timeout, - host_exec_cmd, - host_report_panic, - } -} - -fn host_subscribe(plugin_env: &PluginEnv) { - let mut subscriptions = plugin_env.subscriptions.lock().unwrap(); - let new: HashSet = wasi_read_object(&plugin_env.wasi_env); - subscriptions.extend(new); -} - -fn host_unsubscribe(plugin_env: &PluginEnv) { - let mut subscriptions = plugin_env.subscriptions.lock().unwrap(); - let old: HashSet = wasi_read_object(&plugin_env.wasi_env); - subscriptions.retain(|k| !old.contains(k)); -} - -fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) { - match plugin_env.plugin.run { - PluginType::Pane(Some(tab_index)) => { - let selectable = selectable != 0; - plugin_env - .senders - .send_to_screen(ScreenInstruction::SetSelectable( - PaneId::Plugin(plugin_env.plugin_id), - selectable, - tab_index, - )) - .unwrap() - }, - _ => { - debug!( - "{} - Calling method 'host_set_selectable' does nothing for headless plugins", - plugin_env.plugin.location - ) - }, - } -} - -fn host_get_plugin_ids(plugin_env: &PluginEnv) { - let ids = PluginIds { - plugin_id: plugin_env.plugin_id, - zellij_pid: process::id(), - }; - wasi_write_object(&plugin_env.wasi_env, &ids); -} - -fn host_get_zellij_version(plugin_env: &PluginEnv) { - wasi_write_object(&plugin_env.wasi_env, VERSION); -} - -fn host_open_file(plugin_env: &PluginEnv) { - let path: PathBuf = wasi_read_object(&plugin_env.wasi_env); - plugin_env - .senders - .send_to_pty(PtyInstruction::SpawnTerminal( - Some(TerminalAction::OpenFile(path, None)), - None, - None, - ClientOrTabIndex::TabIndex(plugin_env.tab_index), - )) - .unwrap(); -} - -fn host_switch_tab_to(plugin_env: &PluginEnv, tab_idx: u32) { - plugin_env - .senders - .send_to_screen(ScreenInstruction::GoToTab( - tab_idx, - Some(plugin_env.client_id), - )) - .unwrap(); -} - -fn host_set_timeout(plugin_env: &PluginEnv, secs: f64) { - // There is a fancy, high-performance way to do this with zero additional threads: - // If the plugin thread keeps a BinaryHeap of timer structs, it can manage multiple and easily `.peek()` at the - // next time to trigger in O(1) time. Once the wake-up time is known, the `wasm` thread can use `recv_timeout()` - // to wait for an event with the timeout set to be the time of the next wake up. If events come in in the meantime, - // they are handled, but if the timeout triggers, we replace the event from `recv()` with an - // `Update(pid, TimerEvent)` and pop the timer from the Heap (or reschedule it). No additional threads for as many - // timers as we'd like. - // - // But that's a lot of code, and this is a few lines: - let send_plugin_instructions = plugin_env.senders.to_plugin.clone(); - let update_target = Some(plugin_env.plugin_id); - let client_id = plugin_env.client_id; - thread::spawn(move || { - let start_time = Instant::now(); - thread::sleep(Duration::from_secs_f64(secs)); - // FIXME: The way that elapsed time is being calculated here is not exact; it doesn't take into account the - // time it takes an event to actually reach the plugin after it's sent to the `wasm` thread. - let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64(); - - send_plugin_instructions - .unwrap() - .send(PluginInstruction::Update( - update_target, - Some(client_id), - Event::Timer(elapsed_time), - )) - .unwrap(); - }); -} - -fn host_exec_cmd(plugin_env: &PluginEnv) { - let mut cmdline: Vec = wasi_read_object(&plugin_env.wasi_env); - let command = cmdline.remove(0); - - // Bail out if we're forbidden to run command - if !plugin_env.plugin._allow_exec_host_cmd { - warn!("This plugin isn't allow to run command in host side, skip running this command: '{cmd} {args}'.", - cmd = command, args = cmdline.join(" ")); - return; - } - - // Here, we don't wait the command to finish - process::Command::new(command) - .args(cmdline) - .spawn() - .unwrap(); -} - -// Custom panic handler for plugins. -// -// This is called when a panic occurs in a plugin. Since most panics will likely originate in the -// code trying to deserialize an `Event` upon a plugin state update, we read some panic message, -// formatted as string from the plugin. -fn host_report_panic(plugin_env: &PluginEnv) { - let msg = wasi_read_string(&plugin_env.wasi_env); - panic!("{}", msg); -} - -// Helper Functions --------------------------------------------------------------------------------------------------- - -pub fn wasi_read_string(wasi_env: &WasiEnv) -> String { - let mut state = wasi_env.state(); - let wasi_file = state.fs.stdout_mut().unwrap().as_mut().unwrap(); - let mut buf = String::new(); - wasi_file.read_to_string(&mut buf).unwrap(); - // https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c - buf.replace("\n", "\n\r") -} - -pub fn wasi_write_string(wasi_env: &WasiEnv, buf: &str) { - let mut state = wasi_env.state(); - let wasi_file = state.fs.stdin_mut().unwrap().as_mut().unwrap(); - writeln!(wasi_file, "{}\r", buf).unwrap(); -} - -pub fn wasi_write_object(wasi_env: &WasiEnv, object: &(impl Serialize + ?Sized)) { - wasi_write_string(wasi_env, &serde_json::to_string(&object).unwrap()); -} - -pub fn wasi_read_object(wasi_env: &WasiEnv) -> T { - let json = wasi_read_string(wasi_env); - serde_json::from_str(&json).unwrap() -} diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 86dac577..fe517783 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -285,6 +285,7 @@ pub enum ScreenContext { UpdatePaneName, UndoRenamePane, NewTab, + ApplyLayout, SwitchTabNext, SwitchTabPrev, CloseTab, @@ -350,6 +351,7 @@ pub enum PluginContext { Exit, AddClient, RemoveClient, + NewTab, } /// Stack call representations corresponding to the different types of [`ClientInstruction`]s.