refactor(plugins): fix plugin loading data flow (#1995)
This commit is contained in:
parent
c2a6156a6b
commit
b7adfcc581
20 changed files with 1435 additions and 938 deletions
|
|
@ -4,6 +4,7 @@ pub mod panes;
|
||||||
pub mod tab;
|
pub mod tab;
|
||||||
|
|
||||||
mod logging_pipe;
|
mod logging_pipe;
|
||||||
|
mod plugins;
|
||||||
mod pty;
|
mod pty;
|
||||||
mod pty_writer;
|
mod pty_writer;
|
||||||
mod route;
|
mod route;
|
||||||
|
|
@ -11,7 +12,6 @@ mod screen;
|
||||||
mod terminal_bytes;
|
mod terminal_bytes;
|
||||||
mod thread_bus;
|
mod thread_bus;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod wasm_vm;
|
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use pty_writer::{pty_writer_main, PtyWriteInstruction};
|
use pty_writer::{pty_writer_main, PtyWriteInstruction};
|
||||||
|
|
@ -29,10 +29,10 @@ use wasmer::Store;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
os_input_output::ServerOsApi,
|
os_input_output::ServerOsApi,
|
||||||
|
plugins::{plugin_thread_main, PluginInstruction},
|
||||||
pty::{pty_thread_main, Pty, PtyInstruction},
|
pty::{pty_thread_main, Pty, PtyInstruction},
|
||||||
screen::{screen_thread_main, ScreenInstruction},
|
screen::{screen_thread_main, ScreenInstruction},
|
||||||
thread_bus::{Bus, ThreadSenders},
|
thread_bus::{Bus, ThreadSenders},
|
||||||
wasm_vm::{wasm_thread_main, PluginInstruction},
|
|
||||||
};
|
};
|
||||||
use route::route_thread_main;
|
use route::route_thread_main;
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
|
|
@ -108,7 +108,7 @@ pub(crate) struct SessionMetaData {
|
||||||
pub default_shell: Option<TerminalAction>,
|
pub default_shell: Option<TerminalAction>,
|
||||||
screen_thread: Option<thread::JoinHandle<()>>,
|
screen_thread: Option<thread::JoinHandle<()>>,
|
||||||
pty_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<()>>,
|
pty_writer_thread: Option<thread::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,8 +124,8 @@ impl Drop for SessionMetaData {
|
||||||
if let Some(pty_thread) = self.pty_thread.take() {
|
if let Some(pty_thread) = self.pty_thread.take() {
|
||||||
let _ = pty_thread.join();
|
let _ = pty_thread.join();
|
||||||
}
|
}
|
||||||
if let Some(wasm_thread) = self.wasm_thread.take() {
|
if let Some(plugin_thread) = self.plugin_thread.take() {
|
||||||
let _ = wasm_thread.join();
|
let _ = plugin_thread.join();
|
||||||
}
|
}
|
||||||
if let Some(pty_writer_thread) = self.pty_writer_thread.take() {
|
if let Some(pty_writer_thread) = self.pty_writer_thread.take() {
|
||||||
let _ = pty_writer_thread.join();
|
let _ = pty_writer_thread.join();
|
||||||
|
|
@ -332,7 +332,7 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.senders
|
.senders
|
||||||
.send_to_pty(PtyInstruction::NewTab(
|
.send_to_screen(ScreenInstruction::NewTab(
|
||||||
default_shell.clone(),
|
default_shell.clone(),
|
||||||
tab_layout,
|
tab_layout,
|
||||||
tab_name,
|
tab_name,
|
||||||
|
|
@ -655,6 +655,7 @@ fn init_session(
|
||||||
let pty_thread = thread::Builder::new()
|
let pty_thread = thread::Builder::new()
|
||||||
.name("pty".to_string())
|
.name("pty".to_string())
|
||||||
.spawn({
|
.spawn({
|
||||||
|
let layout = layout.clone();
|
||||||
let pty = Pty::new(
|
let pty = Pty::new(
|
||||||
Bus::new(
|
Bus::new(
|
||||||
vec![pty_receiver],
|
vec![pty_receiver],
|
||||||
|
|
@ -700,7 +701,7 @@ fn init_session(
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let wasm_thread = thread::Builder::new()
|
let plugin_thread = thread::Builder::new()
|
||||||
.name("wasm".to_string())
|
.name("wasm".to_string())
|
||||||
.spawn({
|
.spawn({
|
||||||
let plugin_bus = Bus::new(
|
let plugin_bus = Bus::new(
|
||||||
|
|
@ -715,7 +716,14 @@ fn init_session(
|
||||||
let store = Store::default();
|
let store = Store::default();
|
||||||
|
|
||||||
move || {
|
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();
|
.unwrap();
|
||||||
|
|
@ -750,7 +758,7 @@ fn init_session(
|
||||||
client_attributes,
|
client_attributes,
|
||||||
screen_thread: Some(screen_thread),
|
screen_thread: Some(screen_thread),
|
||||||
pty_thread: Some(pty_thread),
|
pty_thread: Some(pty_thread),
|
||||||
wasm_thread: Some(wasm_thread),
|
plugin_thread: Some(plugin_thread),
|
||||||
pty_writer_thread: Some(pty_writer_thread),
|
pty_writer_thread: Some(pty_writer_thread),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ use crate::{
|
||||||
os_input_output::ServerOsApi,
|
os_input_output::ServerOsApi,
|
||||||
output::{FloatingPanesStack, Output},
|
output::{FloatingPanesStack, Output},
|
||||||
panes::{ActivePanes, PaneId},
|
panes::{ActivePanes, PaneId},
|
||||||
|
plugins::PluginInstruction,
|
||||||
thread_bus::ThreadSenders,
|
thread_bus::ThreadSenders,
|
||||||
ui::pane_contents_and_ui::PaneContentsAndUi,
|
ui::pane_contents_and_ui::PaneContentsAndUi,
|
||||||
wasm_vm::PluginInstruction,
|
|
||||||
ClientId,
|
ClientId,
|
||||||
};
|
};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ use std::time::Instant;
|
||||||
|
|
||||||
use crate::output::{CharacterChunk, SixelImageChunk};
|
use crate::output::{CharacterChunk, SixelImageChunk};
|
||||||
use crate::panes::{grid::Grid, sixel::SixelImageStore, LinkHandler, PaneId};
|
use crate::panes::{grid::Grid, sixel::SixelImageStore, LinkHandler, PaneId};
|
||||||
|
use crate::plugins::PluginInstruction;
|
||||||
use crate::pty::VteBytes;
|
use crate::pty::VteBytes;
|
||||||
use crate::tab::Pane;
|
use crate::tab::Pane;
|
||||||
use crate::ui::pane_boundaries_frame::{FrameParams, PaneFrame};
|
use crate::ui::pane_boundaries_frame::{FrameParams, PaneFrame};
|
||||||
use crate::wasm_vm::PluginInstruction;
|
|
||||||
use crate::ClientId;
|
use crate::ClientId;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ use crate::{
|
||||||
os_input_output::ServerOsApi,
|
os_input_output::ServerOsApi,
|
||||||
output::Output,
|
output::Output,
|
||||||
panes::{ActivePanes, PaneId},
|
panes::{ActivePanes, PaneId},
|
||||||
|
plugins::PluginInstruction,
|
||||||
tab::{Pane, MIN_TERMINAL_HEIGHT, MIN_TERMINAL_WIDTH},
|
tab::{Pane, MIN_TERMINAL_HEIGHT, MIN_TERMINAL_WIDTH},
|
||||||
thread_bus::ThreadSenders,
|
thread_bus::ThreadSenders,
|
||||||
ui::boundaries::Boundaries,
|
ui::boundaries::Boundaries,
|
||||||
ui::pane_contents_and_ui::PaneContentsAndUi,
|
ui::pane_contents_and_ui::PaneContentsAndUi,
|
||||||
wasm_vm::PluginInstruction,
|
|
||||||
ClientId,
|
ClientId,
|
||||||
};
|
};
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
|
|
@ -346,9 +346,7 @@ impl TiledPanes {
|
||||||
self.reset_boundaries();
|
self.reset_boundaries();
|
||||||
}
|
}
|
||||||
pub fn focus_pane_if_client_not_focused(&mut self, pane_id: PaneId, client_id: ClientId) {
|
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() {
|
if self.active_panes.get(&client_id).is_none() {
|
||||||
log::info!("is none");
|
|
||||||
self.focus_pane(pane_id, client_id)
|
self.focus_pane(pane_id, client_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
135
zellij-server/src/plugins/mod.rs
Normal file
135
zellij-server/src/plugins/mod.rs
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
737
zellij-server/src/plugins/wasm_bridge.rs
Normal file
737
zellij-server/src/plugins/wasm_bridge.rs
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::terminal_bytes::TerminalBytes;
|
use crate::terminal_bytes::TerminalBytes;
|
||||||
use crate::{
|
use crate::{
|
||||||
panes::PaneId,
|
panes::PaneId,
|
||||||
|
plugins::PluginInstruction,
|
||||||
screen::ScreenInstruction,
|
screen::ScreenInstruction,
|
||||||
thread_bus::{Bus, ThreadSenders},
|
thread_bus::{Bus, ThreadSenders},
|
||||||
wasm_vm::PluginInstruction,
|
|
||||||
ClientId, ServerInstruction,
|
ClientId, ServerInstruction,
|
||||||
};
|
};
|
||||||
use async_std::task::{self, JoinHandle};
|
use async_std::task::{self, JoinHandle};
|
||||||
|
|
@ -15,7 +15,7 @@ use zellij_utils::{
|
||||||
errors::{ContextType, PtyContext},
|
errors::{ContextType, PtyContext},
|
||||||
input::{
|
input::{
|
||||||
command::{RunCommand, TerminalAction},
|
command::{RunCommand, TerminalAction},
|
||||||
layout::{Layout, PaneLayout, Run},
|
layout::{Layout, PaneLayout, Run, RunPluginLocation},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -51,6 +51,8 @@ pub enum PtyInstruction {
|
||||||
Option<TerminalAction>,
|
Option<TerminalAction>,
|
||||||
Option<PaneLayout>,
|
Option<PaneLayout>,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
|
usize, // tab_index
|
||||||
|
HashMap<RunPluginLocation, Vec<u32>>, // plugin_ids
|
||||||
ClientId,
|
ClientId,
|
||||||
), // the String is the tab name
|
), // the String is the tab name
|
||||||
ClosePane(PaneId),
|
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)
|
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);
|
let err_context = || format!("failed to open new tab for client {}", client_id);
|
||||||
|
|
||||||
pty.spawn_terminals_for_layout(
|
pty.spawn_terminals_for_layout(
|
||||||
tab_layout.unwrap_or_else(|| layout.new_tab()),
|
tab_layout.unwrap_or_else(|| layout.new_tab()),
|
||||||
terminal_action.clone(),
|
terminal_action.clone(),
|
||||||
|
plugin_ids,
|
||||||
|
tab_index,
|
||||||
client_id,
|
client_id,
|
||||||
)
|
)
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?;
|
||||||
|
|
@ -555,6 +566,8 @@ impl Pty {
|
||||||
&mut self,
|
&mut self,
|
||||||
layout: PaneLayout,
|
layout: PaneLayout,
|
||||||
default_shell: Option<TerminalAction>,
|
default_shell: Option<TerminalAction>,
|
||||||
|
plugin_ids: HashMap<RunPluginLocation, Vec<u32>>,
|
||||||
|
tab_index: usize,
|
||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let err_context = || format!("failed to spawn terminals for layout for client {client_id}");
|
let err_context = || format!("failed to spawn terminals for layout for client {client_id}");
|
||||||
|
|
@ -747,9 +760,11 @@ impl Pty {
|
||||||
.collect();
|
.collect();
|
||||||
self.bus
|
self.bus
|
||||||
.senders
|
.senders
|
||||||
.send_to_screen(ScreenInstruction::NewTab(
|
.send_to_screen(ScreenInstruction::ApplyLayout(
|
||||||
layout,
|
layout,
|
||||||
new_tab_pane_ids,
|
new_tab_pane_ids,
|
||||||
|
plugin_ids,
|
||||||
|
tab_index,
|
||||||
client_id,
|
client_id,
|
||||||
))
|
))
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?;
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
os_input_output::ServerOsApi,
|
os_input_output::ServerOsApi,
|
||||||
|
plugins::PluginInstruction,
|
||||||
pty::{ClientOrTabIndex, PtyInstruction},
|
pty::{ClientOrTabIndex, PtyInstruction},
|
||||||
screen::ScreenInstruction,
|
screen::ScreenInstruction,
|
||||||
wasm_vm::PluginInstruction,
|
|
||||||
ServerInstruction, SessionMetaData, SessionState,
|
ServerInstruction, SessionMetaData, SessionState,
|
||||||
};
|
};
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
|
|
@ -440,7 +440,7 @@ pub(crate) fn route_action(
|
||||||
let shell = session.default_shell.clone();
|
let shell = session.default_shell.clone();
|
||||||
session
|
session
|
||||||
.senders
|
.senders
|
||||||
.send_to_pty(PtyInstruction::NewTab(
|
.send_to_screen(ScreenInstruction::NewTab(
|
||||||
shell, tab_layout, tab_name, client_id,
|
shell, tab_layout, tab_name, client_id,
|
||||||
))
|
))
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?;
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ use zellij_utils::errors::prelude::*;
|
||||||
use zellij_utils::input::command::RunCommand;
|
use zellij_utils::input::command::RunCommand;
|
||||||
use zellij_utils::input::options::Clipboard;
|
use zellij_utils::input::options::Clipboard;
|
||||||
use zellij_utils::pane_size::{Size, SizeInPixels};
|
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::alacritty_functions::xparse_color;
|
||||||
use crate::panes::terminal_character::AnsiCode;
|
use crate::panes::terminal_character::AnsiCode;
|
||||||
|
|
@ -18,11 +22,11 @@ use crate::{
|
||||||
output::Output,
|
output::Output,
|
||||||
panes::sixel::SixelImageStore,
|
panes::sixel::SixelImageStore,
|
||||||
panes::PaneId,
|
panes::PaneId,
|
||||||
|
plugins::PluginInstruction,
|
||||||
pty::{ClientOrTabIndex, PtyInstruction, VteBytes},
|
pty::{ClientOrTabIndex, PtyInstruction, VteBytes},
|
||||||
tab::Tab,
|
tab::Tab,
|
||||||
thread_bus::Bus,
|
thread_bus::Bus,
|
||||||
ui::overlay::{Overlay, OverlayWindow, Overlayable},
|
ui::overlay::{Overlay, OverlayWindow, Overlayable},
|
||||||
wasm_vm::PluginInstruction,
|
|
||||||
ClientId, ServerInstruction,
|
ClientId, ServerInstruction,
|
||||||
};
|
};
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
|
|
@ -174,7 +178,19 @@ pub enum ScreenInstruction {
|
||||||
HoldPane(PaneId, Option<i32>, RunCommand, Option<ClientId>), // Option<i32> is the exit status
|
HoldPane(PaneId, Option<i32>, RunCommand, Option<ClientId>), // Option<i32> is the exit status
|
||||||
UpdatePaneName(Vec<u8>, ClientId),
|
UpdatePaneName(Vec<u8>, ClientId),
|
||||||
UndoRenamePane(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),
|
SwitchTabNext(ClientId),
|
||||||
SwitchTabPrev(ClientId),
|
SwitchTabPrev(ClientId),
|
||||||
ToggleActiveSyncTab(ClientId),
|
ToggleActiveSyncTab(ClientId),
|
||||||
|
|
@ -275,6 +291,7 @@ impl From<&ScreenInstruction> for ScreenContext {
|
||||||
ScreenInstruction::UpdatePaneName(..) => ScreenContext::UpdatePaneName,
|
ScreenInstruction::UpdatePaneName(..) => ScreenContext::UpdatePaneName,
|
||||||
ScreenInstruction::UndoRenamePane(..) => ScreenContext::UndoRenamePane,
|
ScreenInstruction::UndoRenamePane(..) => ScreenContext::UndoRenamePane,
|
||||||
ScreenInstruction::NewTab(..) => ScreenContext::NewTab,
|
ScreenInstruction::NewTab(..) => ScreenContext::NewTab,
|
||||||
|
ScreenInstruction::ApplyLayout(..) => ScreenContext::ApplyLayout,
|
||||||
ScreenInstruction::SwitchTabNext(..) => ScreenContext::SwitchTabNext,
|
ScreenInstruction::SwitchTabNext(..) => ScreenContext::SwitchTabNext,
|
||||||
ScreenInstruction::SwitchTabPrev(..) => ScreenContext::SwitchTabPrev,
|
ScreenInstruction::SwitchTabPrev(..) => ScreenContext::SwitchTabPrev,
|
||||||
ScreenInstruction::CloseTab(..) => ScreenContext::CloseTab,
|
ScreenInstruction::CloseTab(..) => ScreenContext::CloseTab,
|
||||||
|
|
@ -761,7 +778,7 @@ impl Screen {
|
||||||
let vte_overlay = overlay.generate_overlay(size).context(err_context)?;
|
let vte_overlay = overlay.generate_overlay(size).context(err_context)?;
|
||||||
tab.render(&mut output, Some(vte_overlay))
|
tab.render(&mut output, Some(vte_overlay))
|
||||||
.context(err_context)?;
|
.context(err_context)?;
|
||||||
} else {
|
} else if !tab.is_pending() {
|
||||||
tabs_to_close.push(*tab_index);
|
tabs_to_close.push(*tab_index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -838,14 +855,8 @@ impl Screen {
|
||||||
self.get_tabs_mut().get_mut(&tab_index)
|
self.get_tabs_mut().get_mut(&tab_index)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new [`Tab`] in this [`Screen`], applying the specified [`Layout`]
|
/// Creates a new [`Tab`] in this [`Screen`]
|
||||||
/// and switching to it.
|
pub fn new_tab(&mut self, tab_index: usize, client_id: ClientId) -> Result<()> {
|
||||||
pub fn new_tab(
|
|
||||||
&mut self,
|
|
||||||
layout: PaneLayout,
|
|
||||||
new_ids: Vec<(u32, HoldForCommand)>,
|
|
||||||
client_id: ClientId,
|
|
||||||
) -> Result<()> {
|
|
||||||
let err_context = || format!("failed to create new tab for client {client_id:?}",);
|
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() {
|
let client_id = if self.get_active_tab(client_id).is_ok() {
|
||||||
|
|
@ -856,9 +867,8 @@ impl Screen {
|
||||||
client_id
|
client_id
|
||||||
};
|
};
|
||||||
|
|
||||||
let tab_index = self.get_new_tab_index();
|
|
||||||
let position = self.tabs.len();
|
let position = self.tabs.len();
|
||||||
let mut tab = Tab::new(
|
let tab = Tab::new(
|
||||||
tab_index,
|
tab_index,
|
||||||
position,
|
position,
|
||||||
String::new(),
|
String::new(),
|
||||||
|
|
@ -882,35 +892,72 @@ impl Screen {
|
||||||
self.terminal_emulator_colors.clone(),
|
self.terminal_emulator_colors.clone(),
|
||||||
self.terminal_emulator_color_codes.clone(),
|
self.terminal_emulator_color_codes.clone(),
|
||||||
);
|
);
|
||||||
tab.apply_layout(layout, new_ids, tab_index, client_id)
|
self.tabs.insert(tab_index, tab);
|
||||||
.with_context(err_context)?;
|
Ok(())
|
||||||
if self.session_is_mirrored {
|
}
|
||||||
if let Ok(active_tab) = self.get_active_tab_mut(client_id) {
|
pub fn apply_layout(
|
||||||
let client_mode_infos_in_source_tab = active_tab.drain_connected_clients(None);
|
&mut self,
|
||||||
tab.add_multiple_clients(client_mode_infos_in_source_tab)
|
layout: PaneLayout,
|
||||||
.with_context(err_context)?;
|
new_terminal_ids: Vec<(u32, HoldForCommand)>,
|
||||||
if active_tab.has_no_connected_clients() {
|
new_plugin_ids: HashMap<RunPluginLocation, Vec<u32>>,
|
||||||
active_tab.visible(false).with_context(err_context)?;
|
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> =
|
let all_connected_clients: Vec<ClientId> =
|
||||||
self.connected_clients.borrow().iter().copied().collect();
|
self.connected_clients.borrow().iter().copied().collect();
|
||||||
for client_id in all_connected_clients {
|
for client_id in all_connected_clients {
|
||||||
self.update_client_tab_focus(client_id, tab_index);
|
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) {
|
} else if let Ok(active_tab) = self.get_active_tab_mut(client_id) {
|
||||||
let client_mode_info_in_source_tab =
|
let client_mode_info_in_source_tab =
|
||||||
active_tab.drain_connected_clients(Some(vec![client_id]));
|
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() {
|
if active_tab.has_no_connected_clients() {
|
||||||
active_tab.visible(false).with_context(err_context)?;
|
active_tab.visible(false).with_context(err_context)?;
|
||||||
}
|
}
|
||||||
self.update_client_tab_focus(client_id, tab_index);
|
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.update_input_modes().with_context(err_context)?;
|
||||||
tab.visible(true).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) {
|
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
|
// 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)?;
|
self.add_client(client_id).with_context(err_context)?;
|
||||||
|
|
@ -1853,8 +1900,28 @@ pub(crate) fn screen_thread_main(
|
||||||
screen.unblock_input()?;
|
screen.unblock_input()?;
|
||||||
screen.render()?;
|
screen.render()?;
|
||||||
},
|
},
|
||||||
ScreenInstruction::NewTab(layout, new_pane_pids, client_id) => {
|
ScreenInstruction::NewTab(default_shell, layout, tab_name, client_id) => {
|
||||||
screen.new_tab(layout, new_pane_pids, 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.unblock_input()?;
|
||||||
screen.render()?;
|
screen.render()?;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,14 @@ use crate::{
|
||||||
panes::sixel::SixelImageStore,
|
panes::sixel::SixelImageStore,
|
||||||
panes::{FloatingPanes, TiledPanes},
|
panes::{FloatingPanes, TiledPanes},
|
||||||
panes::{LinkHandler, PaneId, PluginPane, TerminalPane},
|
panes::{LinkHandler, PaneId, PluginPane, TerminalPane},
|
||||||
|
plugins::PluginInstruction,
|
||||||
pty::{ClientOrTabIndex, PtyInstruction, VteBytes},
|
pty::{ClientOrTabIndex, PtyInstruction, VteBytes},
|
||||||
thread_bus::ThreadSenders,
|
thread_bus::ThreadSenders,
|
||||||
wasm_vm::PluginInstruction,
|
|
||||||
ClientId, ServerInstruction,
|
ClientId, ServerInstruction,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::mpsc::channel;
|
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
|
@ -41,7 +40,7 @@ use zellij_utils::{
|
||||||
data::{Event, InputMode, ModeInfo, Palette, PaletteColor, Style},
|
data::{Event, InputMode, ModeInfo, Palette, PaletteColor, Style},
|
||||||
input::{
|
input::{
|
||||||
command::TerminalAction,
|
command::TerminalAction,
|
||||||
layout::{PaneLayout, Run},
|
layout::{PaneLayout, Run, RunPluginLocation},
|
||||||
parse_keys,
|
parse_keys,
|
||||||
},
|
},
|
||||||
pane_size::{Offset, PaneGeom, Size, SizeInPixels, Viewport},
|
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
|
// FIXME: This should be replaced by `RESIZE_PERCENT` at some point
|
||||||
pub const MIN_TERMINAL_HEIGHT: usize = 5;
|
pub const MIN_TERMINAL_HEIGHT: usize = 5;
|
||||||
pub const MIN_TERMINAL_WIDTH: usize = 5;
|
pub const MIN_TERMINAL_WIDTH: usize = 5;
|
||||||
|
|
||||||
const MAX_PENDING_VTE_EVENTS: usize = 7000;
|
const MAX_PENDING_VTE_EVENTS: usize = 7000;
|
||||||
|
|
||||||
|
type HoldForCommand = Option<RunCommand>;
|
||||||
|
|
||||||
|
enum BufferedTabInstruction {
|
||||||
|
SetPaneSelectable(PaneId, bool),
|
||||||
|
HandlePtyBytes(u32, VteBytes),
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct Tab {
|
pub(crate) struct Tab {
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub position: usize,
|
pub position: usize,
|
||||||
|
|
@ -113,8 +117,11 @@ pub(crate) struct Tab {
|
||||||
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
|
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
|
||||||
pids_waiting_resize: HashSet<u32>, // u32 is the terminal_id
|
pids_waiting_resize: HashSet<u32>, // u32 is the terminal_id
|
||||||
cursor_positions_and_shape: HashMap<ClientId, (usize, usize, String)>, // (x_position,
|
cursor_positions_and_shape: HashMap<ClientId, (usize, usize, String)>, // (x_position,
|
||||||
// y_position,
|
// y_position,
|
||||||
// cursor_shape_csi)
|
// 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)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
|
@ -489,13 +496,16 @@ impl Tab {
|
||||||
terminal_emulator_color_codes,
|
terminal_emulator_color_codes,
|
||||||
pids_waiting_resize: HashSet::new(),
|
pids_waiting_resize: HashSet::new(),
|
||||||
cursor_positions_and_shape: HashMap::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(
|
pub fn apply_layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
layout: PaneLayout,
|
layout: PaneLayout,
|
||||||
new_ids: Vec<(u32, HoldForCommand)>,
|
new_terminal_ids: Vec<(u32, HoldForCommand)>,
|
||||||
|
mut new_plugin_ids: HashMap<RunPluginLocation, Vec<u32>>,
|
||||||
tab_index: usize,
|
tab_index: usize,
|
||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
|
@ -523,7 +533,7 @@ impl Tab {
|
||||||
match layout.position_panes_in_space(&free_space) {
|
match layout.position_panes_in_space(&free_space) {
|
||||||
Ok(positions_in_layout) => {
|
Ok(positions_in_layout) => {
|
||||||
let positions_and_size = positions_in_layout.iter();
|
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 focus_pane_id: Option<PaneId> = None;
|
||||||
let mut set_focus_pane_id = |layout: &PaneLayout, pane_id: PaneId| {
|
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 {
|
for (layout, position_and_size) in positions_and_size {
|
||||||
// A plugin pane
|
// A plugin pane
|
||||||
if let Some(Run::Plugin(run)) = layout.run.clone() {
|
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();
|
let pane_title = run.location.to_string();
|
||||||
self.senders
|
let pid = new_plugin_ids
|
||||||
.send_to_plugin(PluginInstruction::Load(
|
.get_mut(&run.location)
|
||||||
pid_tx,
|
.unwrap()
|
||||||
run,
|
.pop()
|
||||||
tab_index,
|
.unwrap(); // TODO:
|
||||||
client_id,
|
// err_context
|
||||||
position_and_size.into(),
|
// and
|
||||||
))
|
// stuff
|
||||||
.with_context(err_context)?;
|
|
||||||
let pid = pid_rx.recv().with_context(err_context)?;
|
|
||||||
let mut new_plugin = PluginPane::new(
|
let mut new_plugin = PluginPane::new(
|
||||||
pid,
|
pid,
|
||||||
*position_and_size,
|
*position_and_size,
|
||||||
|
|
@ -570,7 +578,7 @@ impl Tab {
|
||||||
set_focus_pane_id(layout, PaneId::Plugin(pid));
|
set_focus_pane_id(layout, PaneId::Plugin(pid));
|
||||||
} else {
|
} else {
|
||||||
// there are still panes left to fill, use the pids we received in this method
|
// 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 next_terminal_position = self.get_next_terminal_position();
|
||||||
let initial_title = match &layout.run {
|
let initial_title = match &layout.run {
|
||||||
Some(Run::Command(run_command)) => Some(run_command.to_string()),
|
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
|
// 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
|
// 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
|
// 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(())
|
Ok(())
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
for (unused_pid, _) in new_ids {
|
for (unused_pid, _) in new_terminal_ids {
|
||||||
self.senders
|
self.senders
|
||||||
.send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(unused_pid)))
|
.send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(unused_pid)))
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?;
|
||||||
}
|
}
|
||||||
|
self.is_pending = false;
|
||||||
Err::<(), _>(anyError::msg(e))
|
Err::<(), _>(anyError::msg(e))
|
||||||
.with_context(err_context)
|
.with_context(err_context)
|
||||||
.non_fatal(); // TODO: propagate this to the user
|
.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<()> {
|
pub fn update_input_modes(&mut self) -> Result<()> {
|
||||||
// this updates all plugins with the client's input mode
|
// this updates all plugins with the client's input mode
|
||||||
let mode_infos = self.mode_info.borrow();
|
let mode_infos = self.mode_info.borrow();
|
||||||
|
|
@ -1120,6 +1146,11 @@ impl Tab {
|
||||||
.any(|s_p| s_p.pid() == PaneId::Plugin(plugin_id))
|
.any(|s_p| s_p.pid() == PaneId::Plugin(plugin_id))
|
||||||
}
|
}
|
||||||
pub fn handle_pty_bytes(&mut self, pid: u32, bytes: VteBytes) -> Result<()> {
|
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}");
|
let err_context = || format!("failed to handle pty bytes from fd {pid}");
|
||||||
if let Some(terminal_output) = self
|
if let Some(terminal_output) = self
|
||||||
.tiled_panes
|
.tiled_panes
|
||||||
|
|
@ -1818,6 +1849,11 @@ impl Tab {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
pub fn set_pane_selectable(&mut self, id: PaneId, selectable: bool) {
|
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) {
|
if let Some(pane) = self.tiled_panes.get_pane_mut(id) {
|
||||||
pane.set_selectable(selectable);
|
pane.set_selectable(selectable);
|
||||||
if !selectable {
|
if !selectable {
|
||||||
|
|
@ -2807,6 +2843,10 @@ impl Tab {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_pending(&self) -> bool {
|
||||||
|
self.is_pending
|
||||||
|
}
|
||||||
|
|
||||||
fn show_floating_panes(&mut self) {
|
fn show_floating_panes(&mut self) {
|
||||||
// this function is to be preferred to directly invoking floating_panes.toggle_show_panes(true)
|
// this function is to be preferred to directly invoking floating_panes.toggle_show_panes(true)
|
||||||
self.floating_panes.toggle_show_panes(true);
|
self.floating_panes.toggle_show_panes(true);
|
||||||
|
|
|
||||||
|
|
@ -224,8 +224,14 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab {
|
||||||
terminal_emulator_colors,
|
terminal_emulator_colors,
|
||||||
terminal_emulator_color_codes,
|
terminal_emulator_color_codes,
|
||||||
);
|
);
|
||||||
tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id)
|
tab.apply_layout(
|
||||||
.unwrap();
|
PaneLayout::default(),
|
||||||
|
vec![(1, None)],
|
||||||
|
HashMap::new(),
|
||||||
|
index,
|
||||||
|
client_id,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
tab
|
tab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,8 +280,14 @@ fn create_new_tab_with_os_api(
|
||||||
terminal_emulator_colors,
|
terminal_emulator_colors,
|
||||||
terminal_emulator_color_codes,
|
terminal_emulator_color_codes,
|
||||||
);
|
);
|
||||||
tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id)
|
tab.apply_layout(
|
||||||
.unwrap();
|
PaneLayout::default(),
|
||||||
|
vec![(1, None)],
|
||||||
|
HashMap::new(),
|
||||||
|
index,
|
||||||
|
client_id,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
tab
|
tab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -328,7 +340,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, _)| (i as u32, None))
|
.map(|(i, _)| (i as u32, None))
|
||||||
.collect();
|
.collect();
|
||||||
tab.apply_layout(tab_layout, pane_ids, index, client_id)
|
tab.apply_layout(tab_layout, pane_ids, HashMap::new(), index, client_id)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
tab
|
tab
|
||||||
}
|
}
|
||||||
|
|
@ -379,8 +391,14 @@ fn create_new_tab_with_mock_pty_writer(
|
||||||
terminal_emulator_colors,
|
terminal_emulator_colors,
|
||||||
terminal_emulator_color_codes,
|
terminal_emulator_color_codes,
|
||||||
);
|
);
|
||||||
tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id)
|
tab.apply_layout(
|
||||||
.unwrap();
|
PaneLayout::default(),
|
||||||
|
vec![(1, None)],
|
||||||
|
HashMap::new(),
|
||||||
|
index,
|
||||||
|
client_id,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
tab
|
tab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,8 +450,14 @@ fn create_new_tab_with_sixel_support(
|
||||||
terminal_emulator_colors,
|
terminal_emulator_colors,
|
||||||
terminal_emulator_color_codes,
|
terminal_emulator_color_codes,
|
||||||
);
|
);
|
||||||
tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id)
|
tab.apply_layout(
|
||||||
.unwrap();
|
PaneLayout::default(),
|
||||||
|
vec![(1, None)],
|
||||||
|
HashMap::new(),
|
||||||
|
index,
|
||||||
|
client_id,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
tab
|
tab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,8 +138,14 @@ fn create_new_tab(size: Size) -> Tab {
|
||||||
terminal_emulator_colors,
|
terminal_emulator_colors,
|
||||||
terminal_emulator_color_codes,
|
terminal_emulator_color_codes,
|
||||||
);
|
);
|
||||||
tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id)
|
tab.apply_layout(
|
||||||
.unwrap();
|
PaneLayout::default(),
|
||||||
|
vec![(1, None)],
|
||||||
|
HashMap::new(),
|
||||||
|
index,
|
||||||
|
client_id,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
tab
|
tab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -185,8 +191,14 @@ fn create_new_tab_with_cell_size(
|
||||||
terminal_emulator_colors,
|
terminal_emulator_colors,
|
||||||
terminal_emulator_color_codes,
|
terminal_emulator_color_codes,
|
||||||
);
|
);
|
||||||
tab.apply_layout(PaneLayout::default(), vec![(1, None)], index, client_id)
|
tab.apply_layout(
|
||||||
.unwrap();
|
PaneLayout::default(),
|
||||||
|
vec![(1, None)],
|
||||||
|
HashMap::new(),
|
||||||
|
index,
|
||||||
|
client_id,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
tab
|
tab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Definitions and helpers for sending and receiving messages between threads.
|
//! Definitions and helpers for sending and receiving messages between threads.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
os_input_output::ServerOsApi, pty::PtyInstruction, pty_writer::PtyWriteInstruction,
|
os_input_output::ServerOsApi, plugins::PluginInstruction, pty::PtyInstruction,
|
||||||
screen::ScreenInstruction, wasm_vm::PluginInstruction, ServerInstruction,
|
pty_writer::PtyWriteInstruction, screen::ScreenInstruction, ServerInstruction,
|
||||||
};
|
};
|
||||||
use zellij_utils::errors::prelude::*;
|
use zellij_utils::errors::prelude::*;
|
||||||
use zellij_utils::{channels, channels::SenderWithContext, errors::ErrorContext};
|
use zellij_utils::{channels, channels::SenderWithContext, errors::ErrorContext};
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ use std::env::set_var;
|
||||||
use std::os::unix::io::RawFd;
|
use std::os::unix::io::RawFd;
|
||||||
use std::sync::{Arc, Mutex};
|
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::ipc::PixelDimensions;
|
||||||
|
|
||||||
use zellij_utils::{
|
use zellij_utils::{
|
||||||
|
|
@ -221,7 +221,7 @@ fn create_new_screen(size: Size) -> Screen {
|
||||||
let session_is_mirrored = true;
|
let session_is_mirrored = true;
|
||||||
let copy_options = CopyOptions::default();
|
let copy_options = CopyOptions::default();
|
||||||
|
|
||||||
Screen::new(
|
let screen = Screen::new(
|
||||||
bus,
|
bus,
|
||||||
&client_attributes,
|
&client_attributes,
|
||||||
max_panes,
|
max_panes,
|
||||||
|
|
@ -229,7 +229,8 @@ fn create_new_screen(size: Size) -> Screen {
|
||||||
draw_pane_frames,
|
draw_pane_frames,
|
||||||
session_is_mirrored,
|
session_is_mirrored,
|
||||||
copy_options,
|
copy_options,
|
||||||
)
|
);
|
||||||
|
screen
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MockScreen {
|
struct MockScreen {
|
||||||
|
|
@ -248,6 +249,7 @@ struct MockScreen {
|
||||||
pub client_attributes: ClientAttributes,
|
pub client_attributes: ClientAttributes,
|
||||||
pub config_options: Options,
|
pub config_options: Options,
|
||||||
pub session_metadata: SessionMetaData,
|
pub session_metadata: SessionMetaData,
|
||||||
|
last_opened_tab_index: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockScreen {
|
impl MockScreen {
|
||||||
|
|
@ -280,27 +282,53 @@ impl MockScreen {
|
||||||
let pane_layout = initial_layout.unwrap_or_default();
|
let pane_layout = initial_layout.unwrap_or_default();
|
||||||
let pane_count = pane_layout.extract_run_instructions().len();
|
let pane_count = pane_layout.extract_run_instructions().len();
|
||||||
let mut pane_ids = vec![];
|
let mut pane_ids = vec![];
|
||||||
|
let plugin_ids = HashMap::new();
|
||||||
for i in 0..pane_count {
|
for i in 0..pane_count {
|
||||||
pane_ids.push((i as u32, None));
|
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(
|
let _ = self.to_screen.send(ScreenInstruction::NewTab(
|
||||||
pane_layout,
|
default_shell,
|
||||||
pane_ids,
|
Some(pane_layout.clone()),
|
||||||
|
tab_name,
|
||||||
self.main_client_id,
|
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
|
screen_thread
|
||||||
}
|
}
|
||||||
pub fn new_tab(&mut self, tab_layout: PaneLayout) {
|
pub fn new_tab(&mut self, tab_layout: PaneLayout) {
|
||||||
let pane_count = tab_layout.extract_run_instructions().len();
|
let pane_count = tab_layout.extract_run_instructions().len();
|
||||||
let mut pane_ids = vec![];
|
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 {
|
for i in 0..pane_count {
|
||||||
pane_ids.push((i as u32, None));
|
pane_ids.push((i as u32, None));
|
||||||
}
|
}
|
||||||
let _ = self.to_screen.send(ScreenInstruction::NewTab(
|
let _ = self.to_screen.send(ScreenInstruction::NewTab(
|
||||||
tab_layout,
|
default_shell,
|
||||||
pane_ids,
|
Some(tab_layout.clone()),
|
||||||
|
tab_name,
|
||||||
self.main_client_id,
|
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<()>>) {
|
pub fn teardown(&mut self, threads: Vec<std::thread::JoinHandle<()>>) {
|
||||||
let _ = self.to_pty.send(PtyInstruction::Exit);
|
let _ = self.to_pty.send(PtyInstruction::Exit);
|
||||||
|
|
@ -321,7 +349,7 @@ impl MockScreen {
|
||||||
default_shell: self.session_metadata.default_shell.clone(),
|
default_shell: self.session_metadata.default_shell.clone(),
|
||||||
screen_thread: None,
|
screen_thread: None,
|
||||||
pty_thread: None,
|
pty_thread: None,
|
||||||
wasm_thread: None,
|
plugin_thread: None,
|
||||||
pty_writer_thread: None,
|
pty_writer_thread: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +397,7 @@ impl MockScreen {
|
||||||
client_attributes: client_attributes.clone(),
|
client_attributes: client_attributes.clone(),
|
||||||
screen_thread: None,
|
screen_thread: None,
|
||||||
pty_thread: None,
|
pty_thread: None,
|
||||||
wasm_thread: None,
|
plugin_thread: None,
|
||||||
pty_writer_thread: None,
|
pty_writer_thread: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -392,6 +420,7 @@ impl MockScreen {
|
||||||
client_attributes,
|
client_attributes,
|
||||||
config_options,
|
config_options,
|
||||||
session_metadata,
|
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 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
|
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");
|
.expect("TEST");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -436,8 +474,8 @@ fn open_new_tab() {
|
||||||
};
|
};
|
||||||
let mut screen = create_new_screen(size);
|
let mut screen = create_new_screen(size);
|
||||||
|
|
||||||
new_tab(&mut screen, 1);
|
new_tab(&mut screen, 1, 0);
|
||||||
new_tab(&mut screen, 2);
|
new_tab(&mut screen, 2, 1);
|
||||||
|
|
||||||
assert_eq!(screen.tabs.len(), 2, "Screen now has two tabs");
|
assert_eq!(screen.tabs.len(), 2, "Screen now has two tabs");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -455,8 +493,8 @@ pub fn switch_to_prev_tab() {
|
||||||
};
|
};
|
||||||
let mut screen = create_new_screen(size);
|
let mut screen = create_new_screen(size);
|
||||||
|
|
||||||
new_tab(&mut screen, 1);
|
new_tab(&mut screen, 1, 1);
|
||||||
new_tab(&mut screen, 2);
|
new_tab(&mut screen, 2, 2);
|
||||||
screen.switch_tab_prev(1).expect("TEST");
|
screen.switch_tab_prev(1).expect("TEST");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -474,8 +512,8 @@ pub fn switch_to_next_tab() {
|
||||||
};
|
};
|
||||||
let mut screen = create_new_screen(size);
|
let mut screen = create_new_screen(size);
|
||||||
|
|
||||||
new_tab(&mut screen, 1);
|
new_tab(&mut screen, 1, 1);
|
||||||
new_tab(&mut screen, 2);
|
new_tab(&mut screen, 2, 2);
|
||||||
screen.switch_tab_prev(1).expect("TEST");
|
screen.switch_tab_prev(1).expect("TEST");
|
||||||
screen.switch_tab_next(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);
|
let mut screen = create_new_screen(size);
|
||||||
|
|
||||||
new_tab(&mut screen, 1);
|
new_tab(&mut screen, 1, 1);
|
||||||
new_tab(&mut screen, 2);
|
new_tab(&mut screen, 2, 2);
|
||||||
screen.close_tab(1).expect("TEST");
|
screen.close_tab(1).expect("TEST");
|
||||||
|
|
||||||
assert_eq!(screen.tabs.len(), 1, "Only one tab left");
|
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);
|
let mut screen = create_new_screen(size);
|
||||||
|
|
||||||
new_tab(&mut screen, 1);
|
new_tab(&mut screen, 1, 1);
|
||||||
new_tab(&mut screen, 2);
|
new_tab(&mut screen, 2, 2);
|
||||||
new_tab(&mut screen, 3);
|
new_tab(&mut screen, 3, 3);
|
||||||
screen.switch_tab_prev(1).expect("TEST");
|
screen.switch_tab_prev(1).expect("TEST");
|
||||||
screen.close_tab(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);
|
let mut screen = create_new_screen(size);
|
||||||
|
|
||||||
new_tab(&mut screen, 1);
|
new_tab(&mut screen, 1, 1);
|
||||||
new_tab(&mut screen, 2);
|
new_tab(&mut screen, 2, 2);
|
||||||
new_tab(&mut screen, 3);
|
new_tab(&mut screen, 3, 3);
|
||||||
screen.switch_tab_prev(1).expect("TEST");
|
screen.switch_tab_prev(1).expect("TEST");
|
||||||
screen.move_focus_left_or_previous_tab(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);
|
let mut screen = create_new_screen(size);
|
||||||
|
|
||||||
new_tab(&mut screen, 1);
|
new_tab(&mut screen, 1, 1);
|
||||||
new_tab(&mut screen, 2);
|
new_tab(&mut screen, 2, 2);
|
||||||
new_tab(&mut screen, 3);
|
new_tab(&mut screen, 3, 3);
|
||||||
screen.switch_tab_prev(1).expect("TEST");
|
screen.switch_tab_prev(1).expect("TEST");
|
||||||
screen.move_focus_right_or_next_tab(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);
|
let mut screen = create_new_screen(position_and_size);
|
||||||
|
|
||||||
new_tab(&mut screen, 1);
|
new_tab(&mut screen, 1, 1);
|
||||||
new_tab(&mut screen, 2);
|
new_tab(&mut screen, 2, 2);
|
||||||
screen.go_to_tab(1, 1).expect("TEST");
|
screen.go_to_tab(1, 1).expect("TEST");
|
||||||
screen.go_to_tab(2, 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);
|
let mut screen = create_new_screen(position_and_size);
|
||||||
|
|
||||||
new_tab(&mut screen, 1);
|
new_tab(&mut screen, 1, 0);
|
||||||
new_tab(&mut screen, 2);
|
new_tab(&mut screen, 2, 1);
|
||||||
new_tab(&mut screen, 3);
|
new_tab(&mut screen, 3, 2);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
screen.tab_history.get(&1).unwrap(),
|
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);
|
let mut screen = create_new_screen(position_and_size);
|
||||||
|
|
||||||
new_tab(&mut screen, 1); // 0
|
new_tab(&mut screen, 1, 0);
|
||||||
new_tab(&mut screen, 2); // 1
|
new_tab(&mut screen, 2, 1);
|
||||||
new_tab(&mut screen, 3); // 2
|
new_tab(&mut screen, 3, 2);
|
||||||
new_tab(&mut screen, 4); // 3
|
new_tab(&mut screen, 4, 3);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
screen.tab_history.get(&1).unwrap(),
|
screen.tab_history.get(&1).unwrap(),
|
||||||
|
|
@ -752,7 +790,7 @@ fn switch_to_tab_with_fullscreen() {
|
||||||
};
|
};
|
||||||
let mut screen = create_new_screen(size);
|
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();
|
let active_tab = screen.get_active_tab_mut(1).unwrap();
|
||||||
active_tab
|
active_tab
|
||||||
|
|
@ -760,7 +798,7 @@ fn switch_to_tab_with_fullscreen() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab.toggle_active_pane_fullscreen(1);
|
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");
|
screen.switch_tab_prev(1).expect("TEST");
|
||||||
|
|
||||||
|
|
@ -867,7 +905,7 @@ fn attach_after_first_tab_closed() {
|
||||||
};
|
};
|
||||||
let mut screen = create_new_screen(size);
|
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();
|
let active_tab = screen.get_active_tab_mut(1).unwrap();
|
||||||
active_tab
|
active_tab
|
||||||
|
|
@ -875,7 +913,7 @@ fn attach_after_first_tab_closed() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
active_tab.toggle_active_pane_fullscreen(1);
|
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.close_tab_at_index(0).expect("TEST");
|
||||||
screen.remove_client(1).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 mut mock_screen = MockScreen::new(size);
|
||||||
let session_metadata = mock_screen.clone_session_metadata();
|
let session_metadata = mock_screen.clone_session_metadata();
|
||||||
let screen_thread = mock_screen.run(Some(initial_layout));
|
let screen_thread = mock_screen.run(Some(initial_layout));
|
||||||
let received_pty_instructions = Arc::new(Mutex::new(vec![]));
|
let received_plugin_instructions = Arc::new(Mutex::new(vec![]));
|
||||||
let pty_receiver = mock_screen.pty_receiver.take().unwrap();
|
let plugin_receiver = mock_screen.plugin_receiver.take().unwrap();
|
||||||
let pty_thread = log_actions_in_thread!(
|
let plugin_thread = log_actions_in_thread!(
|
||||||
received_pty_instructions,
|
received_plugin_instructions,
|
||||||
PtyInstruction::Exit,
|
PluginInstruction::Exit,
|
||||||
pty_receiver
|
plugin_receiver
|
||||||
);
|
);
|
||||||
let new_tab_action = CliAction::NewTab {
|
let new_tab_action = CliAction::NewTab {
|
||||||
name: None,
|
name: None,
|
||||||
|
|
@ -2227,8 +2265,16 @@ pub fn send_cli_new_tab_action_default_params() {
|
||||||
client_id,
|
client_id,
|
||||||
);
|
);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
mock_screen.teardown(vec![pty_thread, screen_thread]);
|
mock_screen.teardown(vec![plugin_thread, screen_thread]);
|
||||||
assert_snapshot!(format!("{:?}", *received_pty_instructions.lock().unwrap()));
|
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]
|
#[test]
|
||||||
|
|
@ -2241,12 +2287,12 @@ pub fn send_cli_new_tab_action_with_name_and_layout() {
|
||||||
let mut mock_screen = MockScreen::new(size);
|
let mut mock_screen = MockScreen::new(size);
|
||||||
let session_metadata = mock_screen.clone_session_metadata();
|
let session_metadata = mock_screen.clone_session_metadata();
|
||||||
let screen_thread = mock_screen.run(Some(initial_layout));
|
let screen_thread = mock_screen.run(Some(initial_layout));
|
||||||
let received_pty_instructions = Arc::new(Mutex::new(vec![]));
|
let received_plugin_instructions = Arc::new(Mutex::new(vec![]));
|
||||||
let pty_receiver = mock_screen.pty_receiver.take().unwrap();
|
let plugin_receiver = mock_screen.plugin_receiver.take().unwrap();
|
||||||
let pty_thread = log_actions_in_thread!(
|
let plugin_thread = log_actions_in_thread!(
|
||||||
received_pty_instructions,
|
received_plugin_instructions,
|
||||||
PtyInstruction::Exit,
|
PluginInstruction::Exit,
|
||||||
pty_receiver
|
plugin_receiver
|
||||||
);
|
);
|
||||||
let new_tab_action = CliAction::NewTab {
|
let new_tab_action = CliAction::NewTab {
|
||||||
name: Some("my-awesome-tab-name".into()),
|
name: Some("my-awesome-tab-name".into()),
|
||||||
|
|
@ -2263,13 +2309,14 @@ pub fn send_cli_new_tab_action_with_name_and_layout() {
|
||||||
client_id,
|
client_id,
|
||||||
);
|
);
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
mock_screen.teardown(vec![pty_thread, screen_thread]);
|
mock_screen.teardown(vec![plugin_thread, screen_thread]);
|
||||||
let new_tab_instruction = received_pty_instructions
|
let new_tab_instruction = received_plugin_instructions
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
|
.rev()
|
||||||
.find(|i| {
|
.find(|i| {
|
||||||
if let PtyInstruction::NewTab(..) = i {
|
if let PluginInstruction::NewTab(..) = i {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,46 @@
|
||||||
---
|
---
|
||||||
source: zellij-server/src/./unit/screen_tests.rs
|
source: zellij-server/src/./unit/screen_tests.rs
|
||||||
assertion_line: 1898
|
assertion_line: 2272
|
||||||
expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())"
|
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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: zellij-server/src/./unit/screen_tests.rs
|
source: zellij-server/src/./unit/screen_tests.rs
|
||||||
assertion_line: 2287
|
assertion_line: 2322
|
||||||
expression: "format!(\"{:#?}\", new_tab_instruction)"
|
expression: "format!(\"{:#?}\", new_tab_instruction)"
|
||||||
---
|
---
|
||||||
NewTab(
|
NewTab(
|
||||||
|
|
@ -63,5 +63,6 @@ NewTab(
|
||||||
Some(
|
Some(
|
||||||
"my-awesome-tab-name",
|
"my-awesome-tab-name",
|
||||||
),
|
),
|
||||||
|
1,
|
||||||
10,
|
10,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: zellij-server/src/./unit/screen_tests.rs
|
source: zellij-server/src/./unit/screen_tests.rs
|
||||||
assertion_line: 2528
|
assertion_line: 2506
|
||||||
expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())"
|
expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())"
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
|
|
@ -18,6 +18,45 @@ expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())"
|
||||||
),
|
),
|
||||||
InputReceived,
|
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(
|
Update(
|
||||||
None,
|
None,
|
||||||
Some(
|
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(
|
Update(
|
||||||
None,
|
None,
|
||||||
Some(
|
Some(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: zellij-server/src/./unit/screen_tests.rs
|
source: zellij-server/src/./unit/screen_tests.rs
|
||||||
assertion_line: 2571
|
assertion_line: 2549
|
||||||
expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())"
|
expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())"
|
||||||
---
|
---
|
||||||
[
|
[
|
||||||
|
|
@ -18,6 +18,45 @@ expression: "format!(\"{:#?}\", * received_plugin_instructions.lock().unwrap())"
|
||||||
),
|
),
|
||||||
InputReceived,
|
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(
|
Update(
|
||||||
None,
|
None,
|
||||||
Some(
|
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(
|
Update(
|
||||||
None,
|
None,
|
||||||
Some(
|
Some(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -285,6 +285,7 @@ pub enum ScreenContext {
|
||||||
UpdatePaneName,
|
UpdatePaneName,
|
||||||
UndoRenamePane,
|
UndoRenamePane,
|
||||||
NewTab,
|
NewTab,
|
||||||
|
ApplyLayout,
|
||||||
SwitchTabNext,
|
SwitchTabNext,
|
||||||
SwitchTabPrev,
|
SwitchTabPrev,
|
||||||
CloseTab,
|
CloseTab,
|
||||||
|
|
@ -350,6 +351,7 @@ pub enum PluginContext {
|
||||||
Exit,
|
Exit,
|
||||||
AddClient,
|
AddClient,
|
||||||
RemoveClient,
|
RemoveClient,
|
||||||
|
NewTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
|
/// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue