aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2022-12-06 15:34:43 +0100
committerGitHub <noreply@github.com>2022-12-06 15:34:43 +0100
commitb7adfcc581d40952ec5d1f5a853669322e895bd7 (patch)
treeba245468ddc56834782604d1829163f8121bd087
parentbuild(deps): bump DeterminateSystems/update-flake-lock from 14 to 15 (#1982) (diff)
downloadsl0ppij-b7adfcc581d40952ec5d1f5a853669322e895bd7.tar.gz
sl0ppij-b7adfcc581d40952ec5d1f5a853669322e895bd7.tar.xz
sl0ppij-b7adfcc581d40952ec5d1f5a853669322e895bd7.zip
refactor(plugins): fix plugin loading data flow (#1995)
-rw-r--r--zellij-server/src/lib.rs26
-rw-r--r--zellij-server/src/panes/floating_panes/mod.rs2
-rw-r--r--zellij-server/src/panes/plugin_pane.rs2
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs4
-rw-r--r--zellij-server/src/plugins/mod.rs135
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs737
-rw-r--r--zellij-server/src/pty.rs23
-rw-r--r--zellij-server/src/route.rs4
-rw-r--r--zellij-server/src/screen.rs129
-rw-r--r--zellij-server/src/tab/mod.rs86
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs42
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs20
-rw-r--r--zellij-server/src/thread_bus.rs4
-rw-r--r--zellij-server/src/unit/screen_tests.rs155
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap46
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap3
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap80
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap80
-rw-r--r--zellij-server/src/wasm_vm.rs785
-rw-r--r--zellij-utils/src/errors.rs2
20 files changed, 1431 insertions, 934 deletions
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<TerminalAction>,
screen_thread: Option<thread::JoinHandle<()>>,
pty_thread: Option<thread::JoinHandle<()>>,
- wasm_thread: Option<thread::JoinHandle<()>>,
+ plugin_thread: Option<thread::JoinHandle<()>>,
pty_writer_thread: Option<thread::JoinHandle<()>>,
}
@@ -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<dyn ServerOsApi>, 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<u32>, Option<ClientId>, 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<TerminalAction>,
+ Option<PaneLayout>,
+ Option<String>, // 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<PluginInstruction>,
+ store: Store,
+ data_dir: PathBuf,
+ plugins: PluginsConfig,
+ layout: Box<Layout>,
+) -> 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<RunPluginLocation, Vec<u32>> = 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<Mutex<HashSet<EventType>>>,
+ 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<ClientId>,
+ plugins: PluginsConfig,
+ senders: ThreadSenders,
+ store: Store,
+ plugin_dir: PathBuf,
+ plugin_cache: HashMap<PathBuf, Module>,
+ 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<ClientId> = vec![];
+ let plugin_cache: HashMap<PathBuf, Module> = 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<u32> {
+ // 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<u32>,
+ cid: Option<ClientId>,
+ 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::<anyError, _>(|e| {
+ match e.downcast::<serde_json::Error>() {
+ 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<EventType> = 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<EventType> = 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<String> = 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<T: DeserializeOwned>(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<TerminalAction>,
Option<PaneLayout>,
Option<String>,
+ usize, // tab_index
+ HashMap<RunPluginLocation, Vec<u32>>, // 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<Layout>) -> 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<TerminalAction>,
+ plugin_ids: HashMap<RunPluginLocation, Vec<u32>>,
+ 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<i32>, RunCommand, Option<ClientId>), // Option<i32> is the exit status
UpdatePaneName(Vec<u8>, ClientId),
UndoRenamePane(ClientId),
- NewTab(PaneLayout, Vec<(u32, HoldForCommand)>, ClientId),
+ NewTab(
+ Option<TerminalAction>,
+ Option<PaneLayout>,
+ Option<String>,
+ ClientId,
+ ),
+ ApplyLayout(
+ PaneLayout,
+ Vec<(u32, HoldForCommand)>,
+ HashMap<RunPluginLocation, Vec<u32>>,
+ 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<RunPluginLocation, Vec<u32>>,
+ 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<ClientId> =
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<RunCommand>;
-
// 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<RunCommand>;
+
+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<RefCell<HashMap<usize, String>>>,
pids_waiting_resize: HashSet<u32>, // u32 is the terminal_id
cursor_positions_and_shape: HashMap<ClientId, (usize, usize, String)>, // (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<BufferedTabInstruction>, // 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<RunPluginLocation, Vec<u32>>,
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<PaneId> = 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<BufferedTabInstruction> =
+ 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<usize>,
}
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(
+ 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(
+ 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<std::thread::JoinHandle<()>>) {
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<u32>, RunPlugin, usize, ClientId, Size), // tx_pid, plugin metadata, tab_index, client_ids
- Update(Option<u32>, Option<ClientId>, 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<Mutex<HashSet<EventType>>>,
- 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<PluginInstruction>,
- 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<ClientId> = 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<PathBuf, Module> = 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::<anyError, _>(|e| match e
- .downcast::<serde_json::Error>()
- {
- 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<PluginInstruction>,
- store: &Store,
- plugin_dir: &Path,
- plugin_cache: &mut HashMap<PathBuf, Module>,
-) -> 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<EventType> = 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<EventType> = 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<String> = 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<T: DeserializeOwned>(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.