feat(config): allow loading background plugins on startup (#3616)

* remove old partial implementation

* feat(plugins): allow loading background plugins on startup

* add e2e test

* update config

* udpate config merging

* style(fmt): rustfmt
This commit is contained in:
Aram Drevekenin 2024-09-20 15:38:20 +02:00 committed by GitHub
parent d92ee89a9d
commit ce8e3995df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 839 additions and 103 deletions

View file

@ -2371,3 +2371,55 @@ pub fn send_command_through_the_cli() {
let last_snapshot = account_for_races_in_snapshot(last_snapshot); let last_snapshot = account_for_races_in_snapshot(last_snapshot);
assert_snapshot!(last_snapshot); assert_snapshot!(last_snapshot);
} }
#[test]
#[ignore]
pub fn load_plugins_in_background_on_startup() {
let fake_win_size = Size {
cols: 120,
rows: 24,
};
let config_file_name = "load_background_plugins.kdl";
let mut test_attempts = 10;
let mut test_timed_out = false;
let last_snapshot = loop {
RemoteRunner::kill_running_sessions(fake_win_size);
let mut runner =
RemoteRunner::new_with_config(fake_win_size, config_file_name).add_step(Step {
name: "Wait for plugin to load and request permissions",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if remote_terminal.snapshot_contains("Allow? (y/n)") {
std::thread::sleep(std::time::Duration::from_millis(100));
remote_terminal.send_key("y".as_bytes());
step_is_complete = true;
}
step_is_complete
},
});
runner.run_all_steps();
let last_snapshot = runner.take_snapshot_after(Step {
name: "Wait for plugin to disappear after permissions were granted",
instruction: |mut remote_terminal: RemoteTerminal| -> bool {
let mut step_is_complete = false;
if !remote_terminal.snapshot_contains("Allow? (y/n)") {
step_is_complete = true;
}
step_is_complete
},
});
if runner.test_timed_out && test_attempts > 0 {
test_attempts -= 1;
continue;
} else {
test_timed_out = runner.test_timed_out;
break last_snapshot;
}
};
let last_snapshot = account_for_races_in_snapshot(last_snapshot);
assert!(
!test_timed_out,
"Test timed out, possibly waiting for permission request"
);
assert_snapshot!(last_snapshot);
}

View file

@ -67,9 +67,13 @@ fn stop_zellij(channel: &mut ssh2::Channel) {
channel.write_all(b"rm -rf /tmp/*\n").unwrap(); // remove temporary artifacts from previous channel.write_all(b"rm -rf /tmp/*\n").unwrap(); // remove temporary artifacts from previous
// tests // tests
channel.write_all(b"rm -rf /tmp/*\n").unwrap(); // remove temporary artifacts from previous channel.write_all(b"rm -rf /tmp/*\n").unwrap(); // remove temporary artifacts from previous
channel.write_all(b"rm -rf /tmp/*\n").unwrap(); // remove temporary artifacts from previous
channel channel
.write_all(b"rm -rf ~/.cache/zellij/*/session_info\n") .write_all(b"rm -rf ~/.cache/zellij/*/session_info\n")
.unwrap(); .unwrap();
channel
.write_all(b"rm -rf ~/.cache/zellij/permissions.kdl\n")
.unwrap();
} }
fn start_zellij(channel: &mut ssh2::Channel) { fn start_zellij(channel: &mut ssh2::Channel) {

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
assertion_line: 2426
expression: last_snapshot
---
Zellij (e2e-test)  Tab #1 
┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│$ █ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Ctrl g  UNLOCK  Alt +  <n> New Pane  <←↓↑→> Change Focus  <f> Floating 

View file

@ -0,0 +1,392 @@
keybinds clear-defaults=true {
locked {
bind "Ctrl g" { SwitchToMode "normal"; }
}
pane {
bind "left" { MoveFocus "left"; }
bind "down" { MoveFocus "down"; }
bind "up" { MoveFocus "up"; }
bind "right" { MoveFocus "right"; }
bind "c" { SwitchToMode "renamepane"; PaneNameInput 0; }
bind "d" { NewPane "down"; SwitchToMode "locked"; }
bind "e" { TogglePaneEmbedOrFloating; SwitchToMode "locked"; }
bind "f" { ToggleFocusFullscreen; SwitchToMode "locked"; }
bind "h" { MoveFocus "left"; }
bind "j" { MoveFocus "down"; }
bind "k" { MoveFocus "up"; }
bind "l" { MoveFocus "right"; }
bind "n" { NewPane; SwitchToMode "locked"; }
bind "p" { SwitchToMode "normal"; }
bind "r" { NewPane "right"; SwitchToMode "locked"; }
bind "w" { ToggleFloatingPanes; SwitchToMode "locked"; }
bind "x" { CloseFocus; SwitchToMode "locked"; }
bind "z" { TogglePaneFrames; SwitchToMode "locked"; }
bind "tab" { SwitchFocus; }
}
tab {
bind "left" { GoToPreviousTab; }
bind "down" { GoToNextTab; }
bind "up" { GoToPreviousTab; }
bind "right" { GoToNextTab; }
bind "1" { GoToTab 1; SwitchToMode "locked"; }
bind "2" { GoToTab 2; SwitchToMode "locked"; }
bind "3" { GoToTab 3; SwitchToMode "locked"; }
bind "4" { GoToTab 4; SwitchToMode "locked"; }
bind "5" { GoToTab 5; SwitchToMode "locked"; }
bind "6" { GoToTab 6; SwitchToMode "locked"; }
bind "7" { GoToTab 7; SwitchToMode "locked"; }
bind "8" { GoToTab 8; SwitchToMode "locked"; }
bind "9" { GoToTab 9; SwitchToMode "locked"; }
bind "[" { BreakPaneLeft; SwitchToMode "locked"; }
bind "]" { BreakPaneRight; SwitchToMode "locked"; }
bind "b" { BreakPane; SwitchToMode "locked"; }
bind "h" { GoToPreviousTab; }
bind "j" { GoToNextTab; }
bind "k" { GoToPreviousTab; }
bind "l" { GoToNextTab; }
bind "n" { NewTab; SwitchToMode "locked"; }
bind "r" { SwitchToMode "renametab"; TabNameInput 0; }
bind "s" { ToggleActiveSyncTab; SwitchToMode "locked"; }
bind "t" { SwitchToMode "normal"; }
bind "x" { CloseTab; SwitchToMode "locked"; }
bind "tab" { ToggleTab; }
}
resize {
bind "left" { Resize "Increase left"; }
bind "down" { Resize "Increase down"; }
bind "up" { Resize "Increase up"; }
bind "right" { Resize "Increase right"; }
bind "+" { Resize "Increase"; }
bind "-" { Resize "Decrease"; }
bind "=" { Resize "Increase"; }
bind "H" { Resize "Decrease left"; }
bind "J" { Resize "Decrease down"; }
bind "K" { Resize "Decrease up"; }
bind "L" { Resize "Decrease right"; }
bind "h" { Resize "Increase left"; }
bind "j" { Resize "Increase down"; }
bind "k" { Resize "Increase up"; }
bind "l" { Resize "Increase right"; }
bind "r" { SwitchToMode "normal"; }
}
move {
bind "left" { MovePane "left"; }
bind "down" { MovePane "down"; }
bind "up" { MovePane "up"; }
bind "right" { MovePane "right"; }
bind "h" { MovePane "left"; }
bind "j" { MovePane "down"; }
bind "k" { MovePane "up"; }
bind "l" { MovePane "right"; }
bind "m" { SwitchToMode "normal"; }
bind "n" { MovePane; }
bind "p" { MovePaneBackwards; }
bind "tab" { MovePane; }
}
scroll {
bind "e" { EditScrollback; SwitchToMode "locked"; }
bind "f" { SwitchToMode "entersearch"; SearchInput 0; }
bind "s" { SwitchToMode "normal"; }
}
search {
bind "c" { SearchToggleOption "CaseSensitivity"; }
bind "n" { Search "down"; }
bind "o" { SearchToggleOption "WholeWord"; }
bind "p" { Search "up"; }
bind "w" { SearchToggleOption "Wrap"; }
}
session {
bind "c" {
LaunchOrFocusPlugin "configuration" {
floating true
move_to_focused_tab true
}
SwitchToMode "locked"
}
bind "d" { Detach; }
bind "o" { SwitchToMode "normal"; }
bind "Ctrl s" { SwitchToMode "scroll"; }
bind "w" {
LaunchOrFocusPlugin "session-manager" {
floating true
move_to_focused_tab true
}
SwitchToMode "locked"
}
}
shared_among "normal" "locked" {
bind "Alt left" { MoveFocusOrTab "left"; }
bind "Alt down" { MoveFocus "down"; }
bind "Alt up" { MoveFocus "up"; }
bind "Alt right" { MoveFocusOrTab "right"; }
bind "Alt +" { Resize "Increase"; }
bind "Alt -" { Resize "Decrease"; }
bind "Alt =" { Resize "Increase"; }
bind "Alt [" { PreviousSwapLayout; }
bind "Alt ]" { NextSwapLayout; }
bind "Alt f" { ToggleFloatingPanes; }
bind "Alt w" {
LaunchPlugin "filepicker" {
close_on_selection true
}
}
bind "Alt h" { MoveFocusOrTab "left"; }
bind "Alt i" { MoveTab "left"; }
bind "Alt j" { MoveFocus "down"; }
bind "Alt k" { MoveFocus "up"; }
bind "Alt l" { MoveFocusOrTab "right"; }
bind "Alt n" { NewPane; }
bind "Alt o" { MoveTab "right"; }
}
shared_except "locked" "renametab" "renamepane" {
bind "Ctrl g" { SwitchToMode "locked"; }
bind "Ctrl q" { Quit; }
}
shared_except "locked" "entersearch" {
bind "enter" { SwitchToMode "locked"; }
}
shared_except "locked" "entersearch" "renametab" "renamepane" "move" {
bind "m" { SwitchToMode "move"; }
}
shared_except "locked" "entersearch" "search" "renametab" "renamepane" "session" {
bind "o" { SwitchToMode "session"; }
}
shared_except "locked" "tab" "entersearch" "renametab" "renamepane" {
bind "t" { SwitchToMode "tab"; }
}
shared_except "locked" "tab" "scroll" "entersearch" "renametab" "renamepane" {
bind "s" { SwitchToMode "scroll"; }
}
shared_except "locked" "pane" "entersearch" "search" "renametab" "renamepane" "move" {
bind "p" { SwitchToMode "pane"; }
}
shared_except "locked" "resize" "pane" "tab" "entersearch" "renametab" "renamepane" {
bind "r" { SwitchToMode "resize"; }
}
shared_among "scroll" "search" {
bind "PageDown" { PageScrollDown; }
bind "PageUp" { PageScrollUp; }
bind "left" { PageScrollUp; }
bind "down" { ScrollDown; }
bind "up" { ScrollUp; }
bind "right" { PageScrollDown; }
bind "Ctrl b" { PageScrollUp; }
bind "Ctrl c" { ScrollToBottom; SwitchToMode "locked"; }
bind "d" { HalfPageScrollDown; }
bind "Ctrl f" { PageScrollDown; }
bind "h" { PageScrollUp; }
bind "j" { ScrollDown; }
bind "k" { ScrollUp; }
bind "l" { PageScrollDown; }
bind "u" { HalfPageScrollUp; }
}
entersearch {
bind "Ctrl c" { SwitchToMode "scroll"; }
bind "esc" { SwitchToMode "scroll"; }
bind "enter" { SwitchToMode "search"; }
}
renametab {
bind "esc" { UndoRenameTab; SwitchToMode "tab"; }
}
shared_among "renametab" "renamepane" {
bind "Ctrl c" { SwitchToMode "locked"; }
}
renamepane {
bind "esc" { UndoRenamePane; SwitchToMode "pane"; }
}
}
plugins {
compact-bar location="zellij:compact-bar"
configuration location="zellij:configuration"
filepicker location="zellij:strider" {
cwd "/"
}
session-manager location="zellij:session-manager"
status-bar location="zellij:status-bar"
strider location="zellij:strider"
tab-bar location="zellij:tab-bar"
welcome-screen location="zellij:session-manager" {
welcome_screen true
}
}
load_plugins {
"file:/usr/src/zellij/wasm32-wasi/release/fixture-plugin-for-tests.wasm" {
config_key "config_value"
config_key2 "config_value2"
}
}
// Use a simplified UI without special fonts (arrow glyphs)
// Options:
// - true
// - false (Default)
//
// simplified_ui true
// Choose the theme that is specified in the themes section.
// Default: default
//
// theme "default"
// Choose the base input mode of zellij.
// Default: normal
//
default_mode "locked"
// Choose the path to the default shell that zellij will use for opening new panes
// Default: $SHELL
//
// default_shell "fish"
// Choose the path to override cwd that zellij will use for opening new panes
//
// default_cwd "/tmp"
// The name of the default layout to load on startup
// Default: "default"
//
// default_layout "compact"
// The folder in which Zellij will look for layouts
//
// layout_dir "/tmp"
// The folder in which Zellij will look for themes
//
// theme_dir "/tmp"
// Toggle enabling the mouse mode.
// On certain configurations, or terminals this could
// potentially interfere with copying text.
// Options:
// - true (default)
// - false
//
// mouse_mode false
// Toggle having pane frames around the panes
// Options:
// - true (default, enabled)
// - false
//
// pane_frames false
// When attaching to an existing session with other users,
// should the session be mirrored (true)
// or should each user have their own cursor (false)
// Default: false
//
// mirror_session true
// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP
// eg. when terminal window with an active zellij session is closed
// Options:
// - detach (Default)
// - quit
//
// on_force_close "quit"
// Configure the scroll back buffer size
// This is the number of lines zellij stores for each pane in the scroll back
// buffer. Excess number of lines are discarded in a FIFO fashion.
// Valid values: positive integers
// Default value: 10000
//
// scroll_buffer_size 10000
// Provide a command to execute when copying text. The text will be piped to
// the stdin of the program to perform the copy. This can be used with
// terminal emulators which do not support the OSC 52 ANSI control sequence
// that will be used by default if this option is not set.
// Examples:
//
// copy_command "xclip -selection clipboard" // x11
// copy_command "wl-copy" // wayland
// copy_command "pbcopy" // osx
//
// Choose the destination for copied text
// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard.
// Does not apply when using copy_command.
// Options:
// - system (default)
// - primary
//
// copy_clipboard "primary"
// Enable automatic copying (and clearing) of selection when releasing mouse
// Default: true
//
// copy_on_select true
// Path to the default editor to use to edit pane scrollbuffer
// Default: $EDITOR or $VISUAL
// scrollback_editor "nvim"
// A fixed name to always give the Zellij session.
// Consider also setting `attach_to_session true,`
// otherwise this will error if such a session exists.
// Default: <RANDOM>
//
// session_name "My singleton session"
// When `session_name` is provided, attaches to that session
// if it is already running or creates it otherwise.
// Default: false
//
// attach_to_session true
// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible
// Options:
// - true (default)
// - false
//
// auto_layout false
// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected
// Options:
// - true (default)
// - false
//
// session_serialization false
// Whether pane viewports are serialized along with the session, default is false
// Options:
// - true
// - false (default)
//
// serialize_pane_viewport false
// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0
// defaults to the scrollback size. If this number is higher than the scrollback size, it will
// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true.
//
// scrollback_lines_to_serialize 10000
// Enable or disable the rendering of styled and colored underlines (undercurl).
// May need to be disabled for certain unsupported terminals
// Default: true
//
// styled_underlines false
// How often in seconds sessions are serialized
//
// serialization_interval 10000
// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know
// metadata info on this session)
// Default: false
//
// disable_session_metadata false
// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it)
// Default: true (if the host terminal supports it)
//
// support_kitty_keyboard_protocol false
// ui {
// pane_frames {
// rounded_corners true
// }
// }

View file

@ -550,6 +550,7 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
}, },
*config.clone(), *config.clone(),
plugin_aliases, plugin_aliases,
client_id,
); );
let mut runtime_configuration = config.clone(); let mut runtime_configuration = config.clone();
runtime_configuration.options = *runtime_config_options.clone(); runtime_configuration.options = *runtime_config_options.clone();
@ -1138,6 +1139,7 @@ fn init_session(
options: SessionOptions, options: SessionOptions,
mut config: Config, mut config: Config,
plugin_aliases: Box<PluginAliases>, plugin_aliases: Box<PluginAliases>,
client_id: ClientId,
) -> SessionMetaData { ) -> SessionMetaData {
let SessionOptions { let SessionOptions {
opts, opts,
@ -1224,7 +1226,8 @@ fn init_session(
.spawn({ .spawn({
let screen_bus = Bus::new( let screen_bus = Bus::new(
vec![screen_receiver, bounded_screen_receiver], vec![screen_receiver, bounded_screen_receiver],
None, Some(&to_screen), // there are certain occasions (eg. caching) where the screen
// needs to send messages to itself
Some(&to_pty), Some(&to_pty),
Some(&to_plugin), Some(&to_plugin),
Some(&to_server), Some(&to_server),
@ -1237,6 +1240,7 @@ fn init_session(
let client_attributes_clone = client_attributes.clone(); let client_attributes_clone = client_attributes.clone();
let debug = opts.debug; let debug = opts.debug;
let layout = layout.clone(); let layout = layout.clone();
let config = config.clone();
move || { move || {
screen_thread_main( screen_thread_main(
screen_bus, screen_bus,
@ -1272,6 +1276,7 @@ fn init_session(
let default_shell = default_shell.clone(); let default_shell = default_shell.clone();
let capabilities = capabilities.clone(); let capabilities = capabilities.clone();
let layout_dir = config_options.layout_dir.clone(); let layout_dir = config_options.layout_dir.clone();
let background_plugins = config.background_plugins.clone();
move || { move || {
plugin_thread_main( plugin_thread_main(
plugin_bus, plugin_bus,
@ -1287,6 +1292,8 @@ fn init_session(
plugin_aliases, plugin_aliases,
default_mode, default_mode,
default_keybinds, default_keybinds,
background_plugins,
client_id,
) )
.fatal() .fatal()
} }

View file

@ -99,6 +99,7 @@ pub(crate) struct PluginPane {
debug: bool, debug: bool,
arrow_fonts: bool, arrow_fonts: bool,
styled_underlines: bool, styled_underlines: bool,
should_be_suppressed: bool,
} }
impl PluginPane { impl PluginPane {
@ -152,6 +153,7 @@ impl PluginPane {
debug, debug,
arrow_fonts, arrow_fonts,
styled_underlines, styled_underlines,
should_be_suppressed: false,
}; };
for client_id in currently_connected_clients { for client_id in currently_connected_clients {
plugin.handle_plugin_bytes(client_id, initial_loading_message.as_bytes().to_vec()); plugin.handle_plugin_bytes(client_id, initial_loading_message.as_bytes().to_vec());
@ -692,6 +694,12 @@ impl Pane for PluginPane {
self.style.rounded_corners = rounded_corners; self.style.rounded_corners = rounded_corners;
self.frame.clear(); self.frame.clear();
} }
fn set_should_be_suppressed(&mut self, should_be_suppressed: bool) {
self.should_be_suppressed = should_be_suppressed;
}
fn query_should_be_suppressed(&self) -> bool {
self.should_be_suppressed
}
} }
impl PluginPane { impl PluginPane {

View file

@ -33,7 +33,7 @@ use zellij_utils::{
command::TerminalAction, command::TerminalAction,
keybinds::Keybinds, keybinds::Keybinds,
layout::{FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, TiledPaneLayout}, layout::{FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, TiledPaneLayout},
plugins::PluginAliases, plugins::{PluginAliases, PluginConfig},
}, },
ipc::ClientAttributes, ipc::ClientAttributes,
pane_size::Size, pane_size::Size,
@ -216,6 +216,12 @@ pub(crate) fn plugin_thread_main(
plugin_aliases: Box<PluginAliases>, plugin_aliases: Box<PluginAliases>,
default_mode: InputMode, default_mode: InputMode,
default_keybinds: Keybinds, default_keybinds: Keybinds,
background_plugins: HashSet<RunPluginOrAlias>,
// the client id that started the session,
// we need it here because the thread's own list of connected clients might not yet be updated
// on session start when we need to load the background plugins, and so we must have an
// explicit client_id that has started the session
initiating_client_id: ClientId,
) -> Result<()> { ) -> Result<()> {
info!("Wasm main thread starts"); info!("Wasm main thread starts");
let plugin_dir = data_dir.join("plugins/"); let plugin_dir = data_dir.join("plugins/");
@ -241,6 +247,16 @@ pub(crate) fn plugin_thread_main(
default_keybinds, default_keybinds,
); );
for mut run_plugin_or_alias in background_plugins {
load_background_plugin(
run_plugin_or_alias,
&mut wasm_bridge,
&bus,
&plugin_aliases,
initiating_client_id,
);
}
loop { loop {
let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel");
err_ctx.add_call(ContextType::Plugin((&event).into())); err_ctx.add_call(ContextType::Plugin((&event).into()));
@ -933,6 +949,53 @@ fn pipe_to_specific_plugins(
} }
} }
fn load_background_plugin(
mut run_plugin_or_alias: RunPluginOrAlias,
wasm_bridge: &mut WasmBridge,
bus: &Bus<PluginInstruction>,
plugin_aliases: &PluginAliases,
client_id: ClientId,
) {
run_plugin_or_alias.populate_run_plugin_if_needed(&plugin_aliases);
let cwd = run_plugin_or_alias.get_initial_cwd();
let run_plugin = run_plugin_or_alias.get_run_plugin();
let size = Size::default();
let skip_cache = false;
match wasm_bridge.load_plugin(
&run_plugin,
None,
size,
cwd.clone(),
skip_cache,
Some(client_id),
None,
) {
Ok((plugin_id, client_id)) => {
let should_float = None;
let should_be_open_in_place = false;
let pane_title = None;
let pane_id_to_replace = None;
let start_suppressed = true;
drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin(
should_float,
should_be_open_in_place,
run_plugin_or_alias,
pane_title,
None,
plugin_id,
pane_id_to_replace,
cwd,
start_suppressed,
// None,
Some(client_id),
)));
},
Err(e) => {
log::error!("Failed to load plugin: {e}");
},
}
}
const EXIT_TIMEOUT: Duration = Duration::from_secs(3); const EXIT_TIMEOUT: Duration = Duration::from_secs(3);
#[path = "./unit/plugin_tests.rs"] #[path = "./unit/plugin_tests.rs"]

View file

@ -836,9 +836,6 @@ impl<'a> PluginLoader<'a> {
))))); )))));
let wasi_ctx = wasi_ctx_builder.build_p1(); let wasi_ctx = wasi_ctx_builder.build_p1();
let mut mut_plugin = self.plugin.clone(); let mut mut_plugin = self.plugin.clone();
if let Some(tab_index) = self.tab_index {
mut_plugin.set_tab_index(tab_index);
}
let plugin_env = PluginEnv { let plugin_env = PluginEnv {
plugin_id: self.plugin_id, plugin_id: self.plugin_id,
client_id: self.client_id, client_id: self.client_id,

View file

@ -301,6 +301,7 @@ fn create_plugin_thread(
Box<dyn FnOnce()>, Box<dyn FnOnce()>,
) { ) {
let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from(".")); let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from("."));
let initiating_client_id = 1;
let (to_server, _server_receiver): ChannelWithContext<ServerInstruction> = let (to_server, _server_receiver): ChannelWithContext<ServerInstruction> =
channels::bounded(50); channels::bounded(50);
let to_server = SenderWithContext::new(to_server); let to_server = SenderWithContext::new(to_server);
@ -367,6 +368,8 @@ fn create_plugin_thread(
Box::new(plugin_aliases), Box::new(plugin_aliases),
InputMode::Normal, InputMode::Normal,
Keybinds::default(), Keybinds::default(),
Default::default(),
initiating_client_id,
) )
.expect("TEST") .expect("TEST")
}) })
@ -430,6 +433,7 @@ fn create_plugin_thread_with_server_receiver(
let plugin_capabilities = PluginCapabilities::default(); let plugin_capabilities = PluginCapabilities::default();
let client_attributes = ClientAttributes::default(); let client_attributes = ClientAttributes::default();
let default_shell_action = None; // TODO: change me let default_shell_action = None; // TODO: change me
let initiating_client_id = 1;
let plugin_thread = std::thread::Builder::new() let plugin_thread = std::thread::Builder::new()
.name("plugin_thread".to_string()) .name("plugin_thread".to_string())
.spawn(move || { .spawn(move || {
@ -448,6 +452,8 @@ fn create_plugin_thread_with_server_receiver(
Box::new(PluginAliases::default()), Box::new(PluginAliases::default()),
InputMode::Normal, InputMode::Normal,
Keybinds::default(), Keybinds::default(),
Default::default(),
initiating_client_id,
) )
.expect("TEST"); .expect("TEST");
}) })
@ -517,6 +523,7 @@ fn create_plugin_thread_with_pty_receiver(
let plugin_capabilities = PluginCapabilities::default(); let plugin_capabilities = PluginCapabilities::default();
let client_attributes = ClientAttributes::default(); let client_attributes = ClientAttributes::default();
let default_shell_action = None; // TODO: change me let default_shell_action = None; // TODO: change me
let initiating_client_id = 1;
let plugin_thread = std::thread::Builder::new() let plugin_thread = std::thread::Builder::new()
.name("plugin_thread".to_string()) .name("plugin_thread".to_string())
.spawn(move || { .spawn(move || {
@ -535,6 +542,8 @@ fn create_plugin_thread_with_pty_receiver(
Box::new(PluginAliases::default()), Box::new(PluginAliases::default()),
InputMode::Normal, InputMode::Normal,
Keybinds::default(), Keybinds::default(),
Default::default(),
initiating_client_id,
) )
.expect("TEST") .expect("TEST")
}) })
@ -599,6 +608,7 @@ fn create_plugin_thread_with_background_jobs_receiver(
let plugin_capabilities = PluginCapabilities::default(); let plugin_capabilities = PluginCapabilities::default();
let client_attributes = ClientAttributes::default(); let client_attributes = ClientAttributes::default();
let default_shell_action = None; // TODO: change me let default_shell_action = None; // TODO: change me
let initiating_client_id = 1;
let plugin_thread = std::thread::Builder::new() let plugin_thread = std::thread::Builder::new()
.name("plugin_thread".to_string()) .name("plugin_thread".to_string())
.spawn(move || { .spawn(move || {
@ -617,6 +627,8 @@ fn create_plugin_thread_with_background_jobs_receiver(
Box::new(PluginAliases::default()), Box::new(PluginAliases::default()),
InputMode::Normal, InputMode::Normal,
Keybinds::default(), Keybinds::default(),
Default::default(),
initiating_client_id,
) )
.expect("TEST") .expect("TEST")
}) })

View file

@ -41,7 +41,6 @@ use zellij_utils::{
actions::Action, actions::Action,
command::{OpenFilePayload, RunCommand, RunCommandAction, TerminalAction}, command::{OpenFilePayload, RunCommand, RunCommandAction, TerminalAction},
layout::{Layout, RunPluginOrAlias}, layout::{Layout, RunPluginOrAlias},
plugins::PluginType,
}, },
plugin_api::{ plugin_api::{
plugin_command::ProtobufPluginCommand, plugin_command::ProtobufPluginCommand,
@ -406,38 +405,25 @@ fn unsubscribe(env: &PluginEnv, event_list: HashSet<EventType>) -> Result<()> {
} }
fn set_selectable(env: &PluginEnv, selectable: bool) { fn set_selectable(env: &PluginEnv, selectable: bool) {
match env.plugin.run { env.senders
PluginType::Pane(Some(tab_index)) => { .send_to_screen(ScreenInstruction::SetSelectable(
// let selectable = selectable != 0; PaneId::Plugin(env.plugin_id),
env.senders selectable,
.send_to_screen(ScreenInstruction::SetSelectable( ))
PaneId::Plugin(env.plugin_id), .with_context(|| {
selectable, format!(
tab_index, "failed to set plugin {} selectable from plugin {}",
)) selectable,
.with_context(|| { env.name()
format!(
"failed to set plugin {} selectable from plugin {}",
selectable,
env.name()
)
})
.non_fatal();
},
_ => {
debug!(
"{} - Calling method 'set_selectable' does nothing for headless plugins",
env.plugin.location
) )
}, })
} .non_fatal();
} }
fn request_permission(env: &PluginEnv, permissions: Vec<PermissionType>) -> Result<()> { fn request_permission(env: &PluginEnv, permissions: Vec<PermissionType>) -> Result<()> {
if PermissionCache::from_path_or_default(None) if PermissionCache::from_path_or_default(None)
.check_permissions(env.plugin.location.to_string(), &permissions) .check_permissions(env.plugin.location.to_string(), &permissions)
{ {
log::info!("PermissionRequestResult 1");
return env return env
.senders .senders
.send_to_plugin(PluginInstruction::PermissionRequestResult( .send_to_plugin(PluginInstruction::PermissionRequestResult(

View file

@ -40,7 +40,7 @@ use crate::{
panes::PaneId, panes::PaneId,
plugins::{PluginId, PluginInstruction, PluginRenderAsset}, plugins::{PluginId, PluginInstruction, PluginRenderAsset},
pty::{ClientTabIndexOrPaneId, PtyInstruction, VteBytes}, pty::{ClientTabIndexOrPaneId, PtyInstruction, VteBytes},
tab::Tab, tab::{SuppressedPanes, Tab},
thread_bus::Bus, thread_bus::Bus,
ui::{ ui::{
loading_indication::LoadingIndication, loading_indication::LoadingIndication,
@ -200,7 +200,7 @@ pub enum ScreenInstruction {
CloseFocusedPane(ClientId), CloseFocusedPane(ClientId),
ToggleActiveTerminalFullscreen(ClientId), ToggleActiveTerminalFullscreen(ClientId),
TogglePaneFrames, TogglePaneFrames,
SetSelectable(PaneId, bool, usize), SetSelectable(PaneId, bool),
ClosePane(PaneId, Option<ClientId>), ClosePane(PaneId, Option<ClientId>),
HoldPane( HoldPane(
PaneId, PaneId,
@ -822,6 +822,27 @@ impl Screen {
Ok(()) Ok(())
} }
fn move_suppressed_panes_from_closed_tab(
&mut self,
suppressed_panes: SuppressedPanes,
) -> Result<()> {
// TODO: this is not entirely accurate, these also sometimes contain a pane who's
// scrollback is being edited - in this case we need to close it or to move it to the
// appropriate tab
let err_context = || "Failed to move suppressed panes from closed tab";
let first_tab_index = *self
.tabs
.keys()
.next()
.context("screen contains no tabs")
.with_context(err_context)?;
self.tabs
.get_mut(&first_tab_index)
.with_context(err_context)?
.add_suppressed_panes(suppressed_panes);
Ok(())
}
fn move_clients_between_tabs( fn move_clients_between_tabs(
&mut self, &mut self,
source_tab_index: usize, source_tab_index: usize,
@ -1053,7 +1074,16 @@ impl Screen {
let err_context = || format!("failed to close tab at index {tab_index:?}"); let err_context = || format!("failed to close tab at index {tab_index:?}");
let mut tab_to_close = self.tabs.remove(&tab_index).with_context(err_context)?; let mut tab_to_close = self.tabs.remove(&tab_index).with_context(err_context)?;
let pane_ids = tab_to_close.get_all_pane_ids(); let mut pane_ids = tab_to_close.get_all_pane_ids();
// here we extract the suppressed panes (these are background panes that don't care which
// tab they are in, and in the future we should probably make them global to screen rather
// than to each tab) and move them to another tab if there is one
let suppressed_panes = tab_to_close.extract_suppressed_panes();
for suppressed_pane_id in suppressed_panes.keys() {
pane_ids.retain(|p| p != suppressed_pane_id);
}
// below we don't check the result of sending the CloseTab instruction to the pty thread // below we don't check the result of sending the CloseTab instruction to the pty thread
// because this might be happening when the app is closing, at which point the pty thread // because this might be happening when the app is closing, at which point the pty thread
// has already closed and this would result in an error // has already closed and this would result in an error
@ -1071,6 +1101,8 @@ impl Screen {
let client_mode_infos_in_closed_tab = tab_to_close.drain_connected_clients(None); let client_mode_infos_in_closed_tab = tab_to_close.drain_connected_clients(None);
self.move_clients_from_closed_tab(client_mode_infos_in_closed_tab) self.move_clients_from_closed_tab(client_mode_infos_in_closed_tab)
.with_context(err_context)?; .with_context(err_context)?;
self.move_suppressed_panes_from_closed_tab(suppressed_panes)
.with_context(err_context)?;
let visible_tab_indices: HashSet<usize> = let visible_tab_indices: HashSet<usize> =
self.active_tab_indices.values().copied().collect(); self.active_tab_indices.values().copied().collect();
for t in self.tabs.values_mut() { for t in self.tabs.values_mut() {
@ -2676,7 +2708,7 @@ pub(crate) fn screen_thread_main(
let mut pending_tab_ids: HashSet<usize> = HashSet::new(); let mut pending_tab_ids: HashSet<usize> = HashSet::new();
let mut pending_tab_switches: HashSet<(usize, ClientId)> = HashSet::new(); // usize is the let mut pending_tab_switches: HashSet<(usize, ClientId)> = HashSet::new(); // usize is the
// tab_index // tab_index
let mut pending_events_waiting_for_client: Vec<ScreenInstruction> = vec![];
let mut plugin_loading_message_cache = HashMap::new(); let mut plugin_loading_message_cache = HashMap::new();
loop { loop {
let (event, mut err_ctx) = screen let (event, mut err_ctx) = screen
@ -3263,18 +3295,14 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::SetSelectable(id, selectable, tab_index) => { ScreenInstruction::SetSelectable(pid, selectable) => {
screen.get_indexed_tab_mut(tab_index).map_or_else( let all_tabs = screen.get_tabs_mut();
|| { for tab in all_tabs.values_mut() {
log::warn!( if tab.has_pane_with_pid(&pid) {
"Tab index #{} not found, could not set selectable for plugin #{:?}.", tab.set_pane_selectable(pid, selectable);
tab_index, break;
id }
) }
},
|tab| tab.set_pane_selectable(id, selectable),
);
screen.render(None)?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
@ -3448,6 +3476,10 @@ pub(crate) fn screen_thread_main(
} }
} }
for event in pending_events_waiting_for_client.drain(..) {
screen.bus.senders.send_to_screen(event).non_fatal();
}
screen.unblock_input()?; screen.unblock_input()?;
screen.render(None)?; screen.render(None)?;
// we do this here in order to recover from a race condition on app start // we do this here in order to recover from a race condition on app start
@ -3674,6 +3706,9 @@ pub(crate) fn screen_thread_main(
} else if let Some(tab_position_to_focus) = tab_position_to_focus { } else if let Some(tab_position_to_focus) = tab_position_to_focus {
screen.go_to_tab(tab_position_to_focus, client_id)?; screen.go_to_tab(tab_position_to_focus, client_id)?;
} }
for event in pending_events_waiting_for_client.drain(..) {
screen.bus.senders.send_to_screen(event).non_fatal();
}
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render(None)?; screen.render(None)?;
}, },
@ -3944,6 +3979,21 @@ pub(crate) fn screen_thread_main(
start_suppressed, start_suppressed,
client_id, client_id,
) => { ) => {
if screen.active_tab_indices.is_empty() && tab_index.is_none() {
pending_events_waiting_for_client.push(ScreenInstruction::AddPlugin(
should_float,
should_be_in_place,
run_plugin_or_alias,
pane_title,
tab_index,
plugin_id,
pane_id_to_replace,
cwd,
start_suppressed,
client_id,
));
continue;
}
let pane_title = pane_title.unwrap_or_else(|| { let pane_title = pane_title.unwrap_or_else(|| {
format!( format!(
"({}) - {}", "({}) - {}",
@ -4208,7 +4258,7 @@ pub(crate) fn screen_thread_main(
let all_tabs = screen.get_tabs_mut(); let all_tabs = screen.get_tabs_mut();
for tab in all_tabs.values_mut() { for tab in all_tabs.values_mut() {
if tab.has_non_suppressed_pane_with_pid(&pane_id) { if tab.has_non_suppressed_pane_with_pid(&pane_id) {
tab.suppress_pane(pane_id, client_id); tab.suppress_pane(pane_id, Some(client_id));
drop(screen.render(None)); drop(screen.render(None));
break; break;
} }
@ -4255,9 +4305,9 @@ pub(crate) fn screen_thread_main(
}); });
if !found { if !found {
log::error!( log::error!("PluginId '{}' not found - caching request", plugin_id);
"PluginId '{}' not found - cannot request permissions", pending_events_waiting_for_client.push(
plugin_id ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission),
); );
} }
}, },

View file

@ -136,6 +136,7 @@ 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>; type HoldForCommand = Option<RunCommand>;
pub type SuppressedPanes = HashMap<PaneId, (bool, Box<dyn Pane>)>; // bool => is scrollback editor
enum BufferedTabInstruction { enum BufferedTabInstruction {
SetPaneSelectable(PaneId, bool), SetPaneSelectable(PaneId, bool),
@ -150,7 +151,7 @@ pub(crate) struct Tab {
pub prev_name: String, pub prev_name: String,
tiled_panes: TiledPanes, tiled_panes: TiledPanes,
floating_panes: FloatingPanes, floating_panes: FloatingPanes,
suppressed_panes: HashMap<PaneId, (bool, Box<dyn Pane>)>, // bool => is scrollback editor suppressed_panes: SuppressedPanes,
max_panes: Option<usize>, max_panes: Option<usize>,
viewport: Rc<RefCell<Viewport>>, // includes all non-UI panes viewport: Rc<RefCell<Viewport>>, // includes all non-UI panes
display_area: Rc<RefCell<Size>>, // includes all panes (including eg. the status bar and tab bar in the default layout) display_area: Rc<RefCell<Size>>, // includes all panes (including eg. the status bar and tab bar in the default layout)
@ -495,6 +496,10 @@ pub trait Pane {
fn update_theme(&mut self, _theme: Palette) {} fn update_theme(&mut self, _theme: Palette) {}
fn update_arrow_fonts(&mut self, _should_support_arrow_fonts: bool) {} fn update_arrow_fonts(&mut self, _should_support_arrow_fonts: bool) {}
fn update_rounded_corners(&mut self, _rounded_corners: bool) {} fn update_rounded_corners(&mut self, _rounded_corners: bool) {}
fn set_should_be_suppressed(&mut self, _should_be_suppressed: bool) {}
fn query_should_be_suppressed(&self) -> bool {
false
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -1855,7 +1860,7 @@ impl Tab {
let mut should_update_ui = false; let mut should_update_ui = false;
let is_sync_panes_active = self.is_sync_panes_active(); let is_sync_panes_active = self.is_sync_panes_active();
let active_terminal = self let active_pane = self
.floating_panes .floating_panes
.get_mut(&pane_id) .get_mut(&pane_id)
.or_else(|| self.tiled_panes.get_pane_mut(pane_id)) .or_else(|| self.tiled_panes.get_pane_mut(pane_id))
@ -1867,8 +1872,7 @@ impl Tab {
// However if the terminal is part of a tab-sync, we need to // However if the terminal is part of a tab-sync, we need to
// check if the terminal should receive input or not (depending on its // check if the terminal should receive input or not (depending on its
// 'exclude_from_sync' configuration). // 'exclude_from_sync' configuration).
let should_not_write_to_terminal = let should_not_write_to_terminal = is_sync_panes_active && active_pane.exclude_from_sync();
is_sync_panes_active && active_terminal.exclude_from_sync();
if should_not_write_to_terminal { if should_not_write_to_terminal {
return Ok(should_update_ui); return Ok(should_update_ui);
@ -1876,7 +1880,7 @@ impl Tab {
match pane_id { match pane_id {
PaneId::Terminal(active_terminal_id) => { PaneId::Terminal(active_terminal_id) => {
match active_terminal.adjust_input_to_terminal( match active_pane.adjust_input_to_terminal(
key_with_modifier, key_with_modifier,
raw_input_bytes, raw_input_bytes,
raw_input_bytes_are_kitty, raw_input_bytes_are_kitty,
@ -1918,7 +1922,7 @@ impl Tab {
None => {}, None => {},
} }
}, },
PaneId::Plugin(pid) => match active_terminal.adjust_input_to_terminal( PaneId::Plugin(pid) => match active_pane.adjust_input_to_terminal(
key_with_modifier, key_with_modifier,
raw_input_bytes, raw_input_bytes,
raw_input_bytes_are_kitty, raw_input_bytes_are_kitty,
@ -1942,6 +1946,10 @@ impl Tab {
.with_context(err_context)?; .with_context(err_context)?;
}, },
Some(AdjustedInput::PermissionRequestResult(permissions, status)) => { Some(AdjustedInput::PermissionRequestResult(permissions, status)) => {
if active_pane.query_should_be_suppressed() {
active_pane.set_should_be_suppressed(false);
self.suppress_pane(PaneId::Plugin(pid), client_id);
}
self.request_plugin_permissions(pid, None); self.request_plugin_permissions(pid, None);
self.senders self.senders
.send_to_plugin(PluginInstruction::PermissionRequestResult( .send_to_plugin(PluginInstruction::PermissionRequestResult(
@ -4113,12 +4121,24 @@ impl Tab {
None => Ok(()), None => Ok(()),
}) })
} }
pub fn suppress_pane(&mut self, pane_id: PaneId, client_id: ClientId) { pub fn focus_suppressed_pane_for_all_clients(&mut self, pane_id: PaneId) {
match self.suppressed_panes.remove(&pane_id) {
Some(pane) => {
self.show_floating_panes();
self.add_floating_pane(pane.1, pane_id, None, None);
self.floating_panes.focus_pane_for_all_clients(pane_id);
},
None => {
log::error!("Could not find suppressed pane wiht id: {:?}", pane_id);
},
}
}
pub fn suppress_pane(&mut self, pane_id: PaneId, client_id: Option<ClientId>) {
// this method places a pane in the suppressed pane with its own ID - this means we'll // this method places a pane in the suppressed pane with its own ID - this means we'll
// not take it out of there when another pane is closed (eg. like happens with the // not take it out of there when another pane is closed (eg. like happens with the
// scrollback editor), but it has to take itself out on its own (eg. a plugin using the // scrollback editor), but it has to take itself out on its own (eg. a plugin using the
// show_self() method) // show_self() method)
if let Some(pane) = self.extract_pane(pane_id, true, Some(client_id)) { if let Some(pane) = self.extract_pane(pane_id, true, client_id) {
let is_scrollback_editor = false; let is_scrollback_editor = false;
self.suppressed_panes self.suppressed_panes
.insert(pane_id, (is_scrollback_editor, pane)); .insert(pane_id, (is_scrollback_editor, pane));
@ -4205,19 +4225,36 @@ impl Tab {
Ok(()) Ok(())
} }
pub fn request_plugin_permissions(&mut self, pid: u32, permissions: Option<PluginPermission>) { pub fn request_plugin_permissions(&mut self, pid: u32, permissions: Option<PluginPermission>) {
let mut should_focus_pane = false;
if let Some(plugin_pane) = self if let Some(plugin_pane) = self
.tiled_panes .tiled_panes
.get_pane_mut(PaneId::Plugin(pid)) .get_pane_mut(PaneId::Plugin(pid))
.or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid)))
.or_else(|| { .or_else(|| {
self.suppressed_panes let mut suppressed_pane = self
.suppressed_panes
.values_mut() .values_mut()
.find(|s_p| s_p.1.pid() == PaneId::Plugin(pid)) .find(|s_p| s_p.1.pid() == PaneId::Plugin(pid))
.map(|s_p| &mut s_p.1) .map(|s_p| &mut s_p.1);
if let Some(suppressed_pane) = suppressed_pane.as_mut() {
if permissions.is_some() {
// here what happens is that we're requesting permissions for a pane that
// is suppressed, meaning the user cannot see the permission request
// so we temporarily focus this pane as a floating pane, marking it so that
// once the permissions are accepted/rejected by the user, it will be
// suppressed again
suppressed_pane.set_should_be_suppressed(true);
should_focus_pane = true;
}
}
suppressed_pane
}) })
{ {
plugin_pane.request_permissions_from_user(permissions); plugin_pane.request_permissions_from_user(permissions);
} }
if should_focus_pane {
self.focus_suppressed_pane_for_all_clients(PaneId::Plugin(pid));
}
} }
pub fn rerun_terminal_pane_with_id(&mut self, terminal_pane_id: u32) { pub fn rerun_terminal_pane_with_id(&mut self, terminal_pane_id: u32) {
let pane_id = PaneId::Terminal(terminal_pane_id); let pane_id = PaneId::Terminal(terminal_pane_id);
@ -4331,6 +4368,14 @@ impl Tab {
pub fn update_auto_layout(&mut self, auto_layout: bool) { pub fn update_auto_layout(&mut self, auto_layout: bool) {
self.auto_layout = auto_layout; self.auto_layout = auto_layout;
} }
pub fn extract_suppressed_panes(&mut self) -> SuppressedPanes {
self.suppressed_panes.drain().collect()
}
pub fn add_suppressed_panes(&mut self, mut suppressed_panes: SuppressedPanes) {
for (pane_id, suppressed_pane_entry) in suppressed_panes.drain() {
self.suppressed_panes.insert(pane_id, suppressed_pane_entry);
}
}
fn new_scrollback_editor_pane(&self, pid: u32) -> TerminalPane { fn new_scrollback_editor_pane(&self, pid: u32) -> TerminalPane {
let next_terminal_position = self.get_next_terminal_position(); let next_terminal_position = self.get_next_terminal_position();
let mut new_pane = TerminalPane::new( let mut new_pane = TerminalPane::new(

View file

@ -210,6 +210,12 @@ plugins {
configuration location="zellij:configuration" configuration location="zellij:configuration"
} }
// Plugins to load in the background when a new session starts
load_plugins {
// "file:/path/to/my-plugin.wasm"
// "https://example.com/my-plugin.wasm"
}
// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP // Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP
// eg. when terminal window with an active zellij session is closed // eg. when terminal window with an active zellij session is closed
// (Requires restart) // (Requires restart)

View file

@ -1,6 +1,7 @@
use crate::data::Palette; use crate::data::Palette;
use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode}; use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fs::File; use std::fs::File;
use std::io::{self, Read}; use std::io::{self, Read};
use std::path::PathBuf; use std::path::PathBuf;
@ -9,6 +10,7 @@ use thiserror::Error;
use std::convert::TryFrom; use std::convert::TryFrom;
use super::keybinds::Keybinds; use super::keybinds::Keybinds;
use super::layout::{RunPlugin, RunPluginOrAlias};
use super::options::Options; use super::options::Options;
use super::plugins::{PluginAliases, PluginsConfigError}; use super::plugins::{PluginAliases, PluginsConfigError};
use super::theme::{Themes, UiConfig}; use super::theme::{Themes, UiConfig};
@ -29,6 +31,7 @@ pub struct Config {
pub plugins: PluginAliases, pub plugins: PluginAliases,
pub ui: UiConfig, pub ui: UiConfig,
pub env: EnvironmentVariables, pub env: EnvironmentVariables,
pub background_plugins: HashSet<RunPluginOrAlias>,
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -400,7 +403,7 @@ mod config_test {
use crate::data::{InputMode, Palette, PaletteColor, PluginTag}; use crate::data::{InputMode, Palette, PaletteColor, PluginTag};
use crate::input::layout::{RunPlugin, RunPluginLocation}; use crate::input::layout::{RunPlugin, RunPluginLocation};
use crate::input::options::{Clipboard, OnForceClose}; use crate::input::options::{Clipboard, OnForceClose};
use crate::input::plugins::{PluginConfig, PluginType}; use crate::input::plugins::PluginConfig;
use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig}; use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::io::Write; use std::io::Write;

View file

@ -35,8 +35,6 @@ impl PluginAliases {
pub struct PluginConfig { pub struct PluginConfig {
/// Path of the plugin, see resolve_wasm_bytes for resolution semantics /// Path of the plugin, see resolve_wasm_bytes for resolution semantics
pub path: PathBuf, pub path: PathBuf,
/// Plugin type
pub run: PluginType,
/// Allow command execution from plugin /// Allow command execution from plugin
pub _allow_exec_host_cmd: bool, pub _allow_exec_host_cmd: bool,
/// Original location of the /// Original location of the
@ -52,7 +50,6 @@ impl PluginConfig {
match &run_plugin.location { match &run_plugin.location {
RunPluginLocation::File(path) => Some(PluginConfig { RunPluginLocation::File(path) => Some(PluginConfig {
path: path.clone(), path: path.clone(),
run: PluginType::Pane(None),
_allow_exec_host_cmd: run_plugin._allow_exec_host_cmd, _allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
location: run_plugin.location.clone(), location: run_plugin.location.clone(),
userspace_configuration: run_plugin.configuration.clone(), userspace_configuration: run_plugin.configuration.clone(),
@ -69,7 +66,6 @@ impl PluginConfig {
{ {
Some(PluginConfig { Some(PluginConfig {
path: PathBuf::from(&tag), path: PathBuf::from(&tag),
run: PluginType::Pane(None),
_allow_exec_host_cmd: run_plugin._allow_exec_host_cmd, _allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
location: RunPluginLocation::parse(&format!("zellij:{}", tag), None) location: RunPluginLocation::parse(&format!("zellij:{}", tag), None)
.ok()?, .ok()?,
@ -82,7 +78,6 @@ impl PluginConfig {
}, },
RunPluginLocation::Remote(_) => Some(PluginConfig { RunPluginLocation::Remote(_) => Some(PluginConfig {
path: PathBuf::new(), path: PathBuf::new(),
run: PluginType::Pane(None),
_allow_exec_host_cmd: run_plugin._allow_exec_host_cmd, _allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
location: run_plugin.location.clone(), location: run_plugin.location.clone(),
userspace_configuration: run_plugin.configuration.clone(), userspace_configuration: run_plugin.configuration.clone(),
@ -187,41 +182,11 @@ impl PluginConfig {
return last_err; return last_err;
} }
/// Sets the tab index inside of the plugin type of the run field.
pub fn set_tab_index(&mut self, tab_index: usize) {
match self.run {
PluginType::Pane(..) => {
self.run = PluginType::Pane(Some(tab_index));
},
PluginType::Headless => {},
}
}
pub fn is_builtin(&self) -> bool { pub fn is_builtin(&self) -> bool {
matches!(self.location, RunPluginLocation::Zellij(_)) matches!(self.location, RunPluginLocation::Zellij(_))
} }
} }
/// Type of the plugin. Defaults to Pane.
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Hash, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum PluginType {
// TODO: A plugin with output that's cloned across every pane in a tab, or across the entire
// application might be useful
// Tab
// Static
/// Starts immediately when Zellij is started and runs without a visible pane
Headless,
/// Runs once per pane declared inside a layout file
Pane(Option<usize>), // tab_index
}
impl Default for PluginType {
fn default() -> Self {
Self::Pane(None)
}
}
#[derive(Error, Debug, PartialEq)] #[derive(Error, Debug, PartialEq)]
pub enum PluginsConfigError { pub enum PluginsConfigError {
#[error("Duplication in plugin tag names is not allowed: '{}'", String::from(.0.clone()))] #[error("Duplication in plugin tag names is not allowed: '{}'", String::from(.0.clone()))]

View file

@ -3616,6 +3616,10 @@ impl Config {
let config_plugins = PluginAliases::from_kdl(kdl_plugin_aliases)?; let config_plugins = PluginAliases::from_kdl(kdl_plugin_aliases)?;
config.plugins.merge(config_plugins); config.plugins.merge(config_plugins);
} }
if let Some(kdl_load_plugins) = kdl_config.get("load_plugins") {
let load_plugins = load_plugins_from_kdl(kdl_load_plugins)?;
config.background_plugins = load_plugins;
}
if let Some(kdl_ui_config) = kdl_config.get("ui") { if let Some(kdl_ui_config) = kdl_config.get("ui") {
let config_ui = UiConfig::from_kdl(&kdl_ui_config)?; let config_ui = UiConfig::from_kdl(&kdl_ui_config)?;
config.ui = config.ui.merge(config_ui); config.ui = config.ui.merge(config_ui);
@ -3640,6 +3644,9 @@ impl Config {
let plugins = self.plugins.to_kdl(add_comments); let plugins = self.plugins.to_kdl(add_comments);
document.nodes_mut().push(plugins); document.nodes_mut().push(plugins);
let load_plugins = load_plugins_to_kdl(&self.background_plugins, add_comments);
document.nodes_mut().push(load_plugins);
if let Some(ui_config) = self.ui.to_kdl() { if let Some(ui_config) = self.ui.to_kdl() {
document.nodes_mut().push(ui_config); document.nodes_mut().push(ui_config);
} }
@ -3729,6 +3736,103 @@ impl PluginAliases {
} }
} }
pub fn load_plugins_to_kdl(
background_plugins: &HashSet<RunPluginOrAlias>,
add_comments: bool,
) -> KdlNode {
let mut load_plugins = KdlNode::new("load_plugins");
let mut load_plugins_children = KdlDocument::new();
for run_plugin_or_alias in background_plugins.iter() {
let mut background_plugin_node = KdlNode::new(run_plugin_or_alias.location_string());
let mut background_plugin_children = KdlDocument::new();
let cwd = match run_plugin_or_alias {
RunPluginOrAlias::RunPlugin(run_plugin) => run_plugin.initial_cwd.clone(),
RunPluginOrAlias::Alias(plugin_alias) => plugin_alias.initial_cwd.clone(),
};
let mut has_children = false;
if let Some(cwd) = cwd.as_ref() {
has_children = true;
let mut cwd_node = KdlNode::new("cwd");
cwd_node.push(cwd.display().to_string());
background_plugin_children.nodes_mut().push(cwd_node);
}
let configuration = match run_plugin_or_alias {
RunPluginOrAlias::RunPlugin(run_plugin) => {
Some(run_plugin.configuration.inner().clone())
},
RunPluginOrAlias::Alias(plugin_alias) => plugin_alias
.configuration
.as_ref()
.map(|c| c.inner().clone()),
};
if let Some(configuration) = configuration {
if !configuration.is_empty() {
has_children = true;
for (config_key, config_value) in configuration {
let mut node = KdlNode::new(config_key.to_owned());
if config_value == "true" {
node.push(KdlValue::Bool(true));
} else if config_value == "false" {
node.push(KdlValue::Bool(false));
} else {
node.push(config_value.to_string());
}
background_plugin_children.nodes_mut().push(node);
}
}
}
if has_children {
background_plugin_node.set_children(background_plugin_children);
}
load_plugins_children
.nodes_mut()
.push(background_plugin_node);
}
load_plugins.set_children(load_plugins_children);
if add_comments {
load_plugins.set_leading(format!(
"\n{}\n{}\n{}\n",
"// Plugins to load in the background when a new session starts",
"// eg. \"file:/path/to/my-plugin.wasm\"",
"// eg. \"https://example.com/my-plugin.wasm\"",
));
}
load_plugins
}
fn load_plugins_from_kdl(
kdl_load_plugins: &KdlNode,
) -> Result<HashSet<RunPluginOrAlias>, ConfigError> {
let mut load_plugins: HashSet<RunPluginOrAlias> = HashSet::new();
if let Some(kdl_load_plugins) = kdl_children_nodes!(kdl_load_plugins) {
for plugin_block in kdl_load_plugins {
let url_node = plugin_block.name();
let string_url = url_node.value();
let configuration = KdlLayoutParser::parse_plugin_user_configuration(&plugin_block)?;
let cwd = kdl_get_string_property_or_child_value!(&plugin_block, "cwd")
.map(|s| PathBuf::from(s));
let run_plugin_or_alias = RunPluginOrAlias::from_url(
&string_url,
&Some(configuration.inner().clone()),
None,
cwd.clone(),
)
.map_err(|e| {
ConfigError::new_kdl_error(
format!("Failed to parse plugin: {}", e),
url_node.span().offset(),
url_node.span().len(),
)
})?
.with_initial_cwd(cwd);
load_plugins.insert(run_plugin_or_alias);
}
}
Ok(load_plugins)
}
impl UiConfig { impl UiConfig {
pub fn from_kdl(kdl_ui_config: &KdlNode) -> Result<UiConfig, ConfigError> { pub fn from_kdl(kdl_ui_config: &KdlNode) -> Result<UiConfig, ConfigError> {
let mut ui_config = UiConfig::default(); let mut ui_config = UiConfig::default();

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/kdl/mod.rs source: zellij-utils/src/kdl/mod.rs
assertion_line: 5060 assertion_line: 5525
expression: fake_config_stringified expression: fake_config_stringified
--- ---
keybinds clear-defaults=true { keybinds clear-defaults=true {
@ -237,4 +237,6 @@ plugins {
welcome_screen true welcome_screen true
} }
} }
load_plugins {
}

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/kdl/mod.rs source: zellij-utils/src/kdl/mod.rs
assertion_line: 5441 assertion_line: 5537
expression: fake_config_stringified expression: fake_config_stringified
--- ---
keybinds clear-defaults=true { keybinds clear-defaults=true {
@ -240,6 +240,12 @@ plugins {
welcome_screen true welcome_screen true
} }
} }
// Plugins to load in the background when a new session starts
// eg. "file:/path/to/my-plugin.wasm"
// eg. "https://example.com/my-plugin.wasm"
load_plugins {
}
// Use a simplified UI without special fonts (arrow glyphs) // Use a simplified UI without special fonts (arrow glyphs)
// Options: // Options:

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/setup.rs source: zellij-utils/src/setup.rs
assertion_line: 753 assertion_line: 754
expression: "format!(\"{:#?}\", config)" expression: "format!(\"{:#?}\", config)"
--- ---
Config { Config {
@ -5682,4 +5682,5 @@ Config {
}, },
}, },
env: {}, env: {},
background_plugins: {},
} }

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/setup.rs source: zellij-utils/src/setup.rs
assertion_line: 811 assertion_line: 812
expression: "format!(\"{:#?}\", config)" expression: "format!(\"{:#?}\", config)"
--- ---
Config { Config {
@ -5686,4 +5686,5 @@ Config {
"LAYOUT_ENV_VAR": "make sure I'm also here", "LAYOUT_ENV_VAR": "make sure I'm also here",
"MY_ENV_VAR": "from layout", "MY_ENV_VAR": "from layout",
}, },
background_plugins: {},
} }

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/setup.rs source: zellij-utils/src/setup.rs
assertion_line: 853 assertion_line: 854
expression: "format!(\"{:#?}\", config)" expression: "format!(\"{:#?}\", config)"
--- ---
Config { Config {
@ -231,4 +231,5 @@ Config {
}, },
}, },
env: {}, env: {},
background_plugins: {},
} }

View file

@ -5989,4 +5989,5 @@ Config {
}, },
}, },
env: {}, env: {},
background_plugins: {},
} }

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/setup.rs source: zellij-utils/src/setup.rs
assertion_line: 825 assertion_line: 826
expression: "format!(\"{:#?}\", config)" expression: "format!(\"{:#?}\", config)"
--- ---
Config { Config {
@ -5682,4 +5682,5 @@ Config {
}, },
}, },
env: {}, env: {},
background_plugins: {},
} }