aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2024-09-20 15:38:20 +0200
committerGitHub <noreply@github.com>2024-09-20 15:38:20 +0200
commitce8e3995df9e554250543e1a8d90e2e053e3dbec (patch)
tree5c2be78e50d0fac36f118931c5112dee5ef6fe16
parentdocs(changelog): break multiple panes APIs (diff)
downloadsl0ppij-ce8e3995df9e554250543e1a8d90e2e053e3dbec.tar.gz
sl0ppij-ce8e3995df9e554250543e1a8d90e2e053e3dbec.tar.xz
sl0ppij-ce8e3995df9e554250543e1a8d90e2e053e3dbec.zip
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
-rw-r--r--src/tests/e2e/cases.rs52
-rw-r--r--src/tests/e2e/remote_runner.rs4
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__load_plugins_in_background_on_startup.snap29
-rw-r--r--src/tests/fixtures/configs/load_background_plugins.kdl392
-rw-r--r--zellij-server/src/lib.rs9
-rw-r--r--zellij-server/src/panes/plugin_pane.rs8
-rw-r--r--zellij-server/src/plugins/mod.rs65
-rw-r--r--zellij-server/src/plugins/plugin_loader.rs3
-rw-r--r--zellij-server/src/plugins/unit/plugin_tests.rs12
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs38
-rw-r--r--zellij-server/src/screen.rs90
-rw-r--r--zellij-server/src/tab/mod.rs65
-rw-r--r--zellij-utils/assets/config/default.kdl6
-rw-r--r--zellij-utils/src/input/config.rs5
-rw-r--r--zellij-utils/src/input/plugins.rs35
-rw-r--r--zellij-utils/src/kdl/mod.rs104
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap4
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap8
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap3
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap3
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap3
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap1
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap3
23 files changed, 839 insertions, 103 deletions
diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs
index eec66096..5103d656 100644
--- a/src/tests/e2e/cases.rs
+++ b/src/tests/e2e/cases.rs
@@ -2371,3 +2371,55 @@ pub fn send_command_through_the_cli() {
let last_snapshot = account_for_races_in_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);
+}
diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs
index 49cdf64e..40feeb21 100644
--- a/src/tests/e2e/remote_runner.rs
+++ b/src/tests/e2e/remote_runner.rs
@@ -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
// 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 ~/.cache/zellij/*/session_info\n")
.unwrap();
+ channel
+ .write_all(b"rm -rf ~/.cache/zellij/permissions.kdl\n")
+ .unwrap();
}
fn start_zellij(channel: &mut ssh2::Channel) {
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__load_plugins_in_background_on_startup.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__load_plugins_in_background_on_startup.snap
new file mode 100644
index 00000000..cabf017a
--- /dev/null
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__load_plugins_in_background_on_startup.snap
@@ -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 
diff --git a/src/tests/fixtures/configs/load_background_plugins.kdl b/src/tests/fixtures/configs/load_background_plugins.kdl
new file mode 100644
index 00000000..dc270d9f
--- /dev/null
+++ b/src/tests/fixtures/configs/load_background_plugins.kdl
@@ -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
+ // }
+ // }
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs
index 470ea88d..d95dff7b 100644
--- a/zellij-server/src/lib.rs
+++ b/zellij-server/src/lib.rs
@@ -550,6 +550,7 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
},
*config.clone(),
plugin_aliases,
+ client_id,
);
let mut runtime_configuration = config.clone();
runtime_configuration.options = *runtime_config_options.clone();
@@ -1138,6 +1139,7 @@ fn init_session(
options: SessionOptions,
mut config: Config,
plugin_aliases: Box<PluginAliases>,
+ client_id: ClientId,
) -> SessionMetaData {
let SessionOptions {
opts,
@@ -1224,7 +1226,8 @@ fn init_session(
.spawn({
let screen_bus = Bus::new(
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_plugin),
Some(&to_server),
@@ -1237,6 +1240,7 @@ fn init_session(
let client_attributes_clone = client_attributes.clone();
let debug = opts.debug;
let layout = layout.clone();
+ let config = config.clone();
move || {
screen_thread_main(
screen_bus,
@@ -1272,6 +1276,7 @@ fn init_session(
let default_shell = default_shell.clone();
let capabilities = capabilities.clone();
let layout_dir = config_options.layout_dir.clone();
+ let background_plugins = config.background_plugins.clone();
move || {
plugin_thread_main(
plugin_bus,
@@ -1287,6 +1292,8 @@ fn init_session(
plugin_aliases,
default_mode,
default_keybinds,
+ background_plugins,
+ client_id,
)
.fatal()
}
diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs
index 5ef548c2..7e16ee2d 100644
--- a/zellij-server/src/panes/plugin_pane.rs
+++ b/zellij-server/src/panes/plugin_pane.rs
@@ -99,6 +99,7 @@ pub(crate) struct PluginPane {
debug: bool,
arrow_fonts: bool,
styled_underlines: bool,
+ should_be_suppressed: bool,
}
impl PluginPane {
@@ -152,6 +153,7 @@ impl PluginPane {
debug,
arrow_fonts,
styled_underlines,
+ should_be_suppressed: false,
};
for client_id in currently_connected_clients {
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.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 {
diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs
index 9f7ef13a..af2723a0 100644
--- a/zellij-server/src/plugins/mod.rs
+++ b/zellij-server/src/plugins/mod.rs
@@ -33,7 +33,7 @@ use zellij_utils::{
command::TerminalAction,
keybinds::Keybinds,
layout::{FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, TiledPaneLayout},
- plugins::PluginAliases,
+ plugins::{PluginAliases, PluginConfig},
},
ipc::ClientAttributes,
pane_size::Size,
@@ -216,6 +216,12 @@ pub(crate) fn plugin_thread_main(
plugin_aliases: Box<PluginAliases>,
default_mode: InputMode,
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<()> {
info!("Wasm main thread starts");
let plugin_dir = data_dir.join("plugins/");
@@ -241,6 +247,16 @@ pub(crate) fn plugin_thread_main(
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 {
let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel");
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);
#[path = "./unit/plugin_tests.rs"]
diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs
index 7613da09..41bdb7f6 100644
--- a/zellij-server/src/plugins/plugin_loader.rs
+++ b/zellij-server/src/plugins/plugin_loader.rs
@@ -836,9 +836,6 @@ impl<'a> PluginLoader<'a> {
)))));
let wasi_ctx = wasi_ctx_builder.build_p1();
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 {
plugin_id: self.plugin_id,
client_id: self.client_id,
diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs
index 0d20cea0..690ef516 100644
--- a/zellij-server/src/plugins/unit/plugin_tests.rs
+++ b/zellij-server/src/plugins/unit/plugin_tests.rs
@@ -301,6 +301,7 @@ fn create_plugin_thread(
Box<dyn FnOnce()>,
) {
let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from("."));
+ let initiating_client_id = 1;
let (to_server, _server_receiver): ChannelWithContext<ServerInstruction> =
channels::bounded(50);
let to_server = SenderWithContext::new(to_server);
@@ -367,6 +368,8 @@ fn create_plugin_thread(
Box::new(plugin_aliases),
InputMode::Normal,
Keybinds::default(),
+ Default::default(),
+ initiating_client_id,
)
.expect("TEST")
})
@@ -430,6 +433,7 @@ fn create_plugin_thread_with_server_receiver(
let plugin_capabilities = PluginCapabilities::default();
let client_attributes = ClientAttributes::default();
let default_shell_action = None; // TODO: change me
+ let initiating_client_id = 1;
let plugin_thread = std::thread::Builder::new()
.name("plugin_thread".to_string())
.spawn(move || {
@@ -448,6 +452,8 @@ fn create_plugin_thread_with_server_receiver(
Box::new(PluginAliases::default()),
InputMode::Normal,
Keybinds::default(),
+ Default::default(),
+ initiating_client_id,
)
.expect("TEST");
})
@@ -517,6 +523,7 @@ fn create_plugin_thread_with_pty_receiver(
let plugin_capabilities = PluginCapabilities::default();
let client_attributes = ClientAttributes::default();
let default_shell_action = None; // TODO: change me
+ let initiating_client_id = 1;
let plugin_thread = std::thread::Builder::new()
.name("plugin_thread".to_string())
.spawn(move || {
@@ -535,6 +542,8 @@ fn create_plugin_thread_with_pty_receiver(
Box::new(PluginAliases::default()),
InputMode::Normal,
Keybinds::default(),
+ Default::default(),
+ initiating_client_id,
)
.expect("TEST")
})
@@ -599,6 +608,7 @@ fn create_plugin_thread_with_background_jobs_receiver(
let plugin_capabilities = PluginCapabilities::default();
let client_attributes = ClientAttributes::default();
let default_shell_action = None; // TODO: change me
+ let initiating_client_id = 1;
let plugin_thread = std::thread::Builder::new()
.name("plugin_thread".to_string())
.spawn(move || {
@@ -617,6 +627,8 @@ fn create_plugin_thread_with_background_jobs_receiver(
Box::new(PluginAliases::default()),
InputMode::Normal,
Keybinds::default(),
+ Default::default(),
+ initiating_client_id,
)
.expect("TEST")
})
diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs
index 6bdabaa3..d1e938a2 100644
--- a/zellij-server/src/plugins/zellij_exports.rs
+++ b/zellij-server/src/plugins/zellij_exports.rs
@@ -41,7 +41,6 @@ use zellij_utils::{
actions::Action,
command::{OpenFilePayload, RunCommand, RunCommandAction, TerminalAction},
layout::{Layout, RunPluginOrAlias},
- plugins::PluginType,
},
plugin_api::{
plugin_command::ProtobufPluginCommand,
@@ -406,38 +405,25 @@ fn unsubscribe(env: &PluginEnv, event_list: HashSet<EventType>) -> Result<()> {
}
fn set_selectable(env: &PluginEnv, selectable: bool) {
- match env.plugin.run {
- PluginType::Pane(Some(tab_index)) => {
- // let selectable = selectable != 0;
- env.senders
- .send_to_screen(ScreenInstruction::SetSelectable(
- PaneId::Plugin(env.plugin_id),
- selectable,
- tab_index,
- ))
- .with_context(|| {
- 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
+ env.senders
+ .send_to_screen(ScreenInstruction::SetSelectable(
+ PaneId::Plugin(env.plugin_id),
+ selectable,
+ ))
+ .with_context(|| {
+ format!(
+ "failed to set plugin {} selectable from plugin {}",
+ selectable,
+ env.name()
)
- },
- }
+ })
+ .non_fatal();
}
fn request_permission(env: &PluginEnv, permissions: Vec<PermissionType>) -> Result<()> {
if PermissionCache::from_path_or_default(None)
.check_permissions(env.plugin.location.to_string(), &permissions)
{
- log::info!("PermissionRequestResult 1");
return env
.senders
.send_to_plugin(PluginInstruction::PermissionRequestResult(
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index cebe5b26..e7b0cb7b 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -40,7 +40,7 @@ use crate::{
panes::PaneId,
plugins::{PluginId, PluginInstruction, PluginRenderAsset},
pty::{ClientTabIndexOrPaneId, PtyInstruction, VteBytes},
- tab::Tab,
+ tab::{SuppressedPanes, Tab},
thread_bus::Bus,
ui::{
loading_indication::LoadingIndication,
@@ -200,7 +200,7 @@ pub enum ScreenInstruction {
CloseFocusedPane(ClientId),
ToggleActiveTerminalFullscreen(ClientId),
TogglePaneFrames,
- SetSelectable(PaneId, bool, usize),
+ SetSelectable(PaneId, bool),
ClosePane(PaneId, Option<ClientId>),
HoldPane(
PaneId,
@@ -822,6 +822,27 @@ impl Screen {
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(
&mut self,
source_tab_index: usize,
@@ -1053,7 +1074,16 @@ impl Screen {
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 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
// 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
@@ -1071,6 +1101,8 @@ impl Screen {
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)
.with_context(err_context)?;
+ self.move_suppressed_panes_from_closed_tab(suppressed_panes)
+ .with_context(err_context)?;
let visible_tab_indices: HashSet<usize> =
self.active_tab_indices.values().copied().collect();
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_switches: HashSet<(usize, ClientId)> = HashSet::new(); // usize is the
// tab_index
-
+ let mut pending_events_waiting_for_client: Vec<ScreenInstruction> = vec![];
let mut plugin_loading_message_cache = HashMap::new();
loop {
let (event, mut err_ctx) = screen
@@ -3263,18 +3295,14 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?;
screen.log_and_report_session_state()?;
},
- ScreenInstruction::SetSelectable(id, selectable, tab_index) => {
- screen.get_indexed_tab_mut(tab_index).map_or_else(
- || {
- log::warn!(
- "Tab index #{} not found, could not set selectable for plugin #{:?}.",
- tab_index,
- id
- )
- },
- |tab| tab.set_pane_selectable(id, selectable),
- );
-
+ ScreenInstruction::SetSelectable(pid, selectable) => {
+ let all_tabs = screen.get_tabs_mut();
+ for tab in all_tabs.values_mut() {
+ if tab.has_pane_with_pid(&pid) {
+ tab.set_pane_selectable(pid, selectable);
+ break;
+ }
+ }
screen.render(None)?;
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.render(None)?;
// 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 {
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.render(None)?;
},
@@ -3944,6 +3979,21 @@ pub(crate) fn screen_thread_main(
start_suppressed,
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(|| {
format!(
"({}) - {}",
@@ -4208,7 +4258,7 @@ pub(crate) fn screen_thread_main(
let all_tabs = screen.get_tabs_mut();
for tab in all_tabs.values_mut() {
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));
break;
}
@@ -4255,9 +4305,9 @@ pub(crate) fn screen_thread_main(
});
if !found {
- log::error!(
- "PluginId '{}' not found - cannot request permissions",
- plugin_id
+ log::error!("PluginId '{}' not found - caching request", plugin_id);
+ pending_events_waiting_for_client.push(
+ ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission),
);
}
},
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index ae238fa0..9cabfabe 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -136,6 +136,7 @@ pub const MIN_TERMINAL_WIDTH: usize = 5;
const MAX_PENDING_VTE_EVENTS: usize = 7000;
type HoldForCommand = Option<RunCommand>;
+pub type SuppressedPanes = HashMap<PaneId, (bool, Box<dyn Pane>)>; // bool => is scrollback editor
enum BufferedTabInstruction {
SetPaneSelectable(PaneId, bool),
@@ -150,7 +151,7 @@ pub(crate) struct Tab {
pub prev_name: String,
tiled_panes: TiledPanes,
floating_panes: FloatingPanes,
- suppressed_panes: HashMap<PaneId, (bool, Box<dyn Pane>)>, // bool => is scrollback editor
+ suppressed_panes: SuppressedPanes,
max_panes: Option<usize>,
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)
@@ -495,6 +496,10 @@ pub trait Pane {
fn update_theme(&mut self, _theme: Palette) {}
fn update_arrow_fonts(&mut self, _should_support_arrow_fonts: 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)]
@@ -1855,7 +1860,7 @@ impl Tab {
let mut should_update_ui = false;
let is_sync_panes_active = self.is_sync_panes_active();
- let active_terminal = self
+ let active_pane = self
.floating_panes
.get_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
// check if the terminal should receive input or not (depending on its
// 'exclude_from_sync' configuration).
- let should_not_write_to_terminal =
- is_sync_panes_active && active_terminal.exclude_from_sync();
+ let should_not_write_to_terminal = is_sync_panes_active && active_pane.exclude_from_sync();
if should_not_write_to_terminal {
return Ok(should_update_ui);
@@ -1876,7 +1880,7 @@ impl Tab {
match pane_id {
PaneId::Terminal(active_terminal_id) => {
- match active_terminal.adjust_input_to_terminal(
+ match active_pane.adjust_input_to_terminal(
key_with_modifier,
raw_input_bytes,
raw_input_bytes_are_kitty,
@@ -1918,7 +1922,7 @@ impl Tab {
None => {},
}
},
- PaneId::Plugin(pid) => match active_terminal.adjust_input_to_terminal(
+ PaneId::Plugin(pid) => match active_pane.adjust_input_to_terminal(
key_with_modifier,
raw_input_bytes,
raw_input_bytes_are_kitty,
@@ -1942,6 +1946,10 @@ impl Tab {
.with_context(err_context)?;
},
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.senders
.send_to_plugin(PluginInstruction::PermissionRequestResult(
@@ -4113,12 +4121,24 @@ impl Tab {
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
// 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
// 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;
self.suppressed_panes
.insert(pane_id, (is_scrollback_editor, pane));
@@ -4205,19 +4225,36 @@ impl Tab {
Ok(())
}
pub fn request_plugin_permissions(&mut self, pid: u32, permissions: Option<PluginPermission>) {
+ let mut should_focus_pane = false;
if let Some(plugin_pane) = self
.tiled_panes
.get_pane_mut(PaneId::Plugin(pid))
.or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid)))
.or_else(|| {
- self.suppressed_panes
+ let mut suppressed_pane = self
+ .suppressed_panes
.values_mut()
.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);
}
+ 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) {
let pane_id = PaneId::Terminal(terminal_pane_id);
@@ -4331,6 +4368,14 @@ impl Tab {
pub fn update_auto_layout(&mut self, auto_layout: bool) {
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 {
let next_terminal_position = self.get_next_terminal_position();
let mut new_pane = TerminalPane::new(
diff --git a/zellij-utils/assets/config/default.kdl b/zellij-utils/assets/config/default.kdl
index 9d2afa40..2765c78e 100644
--- a/zellij-utils/assets/config/default.kdl
+++ b/zellij-utils/assets/config/default.kdl
@@ -210,6 +210,12 @@ plugins {
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
// eg. when terminal window with an active zellij session is closed
// (Requires restart)
diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs
index 49ff5c2e..ae574b99 100644
--- a/zellij-utils/src/input/config.rs
+++ b/zellij-utils/src/input/config.rs
@@ -1,6 +1,7 @@
use crate::data::Palette;
use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
use serde::{Deserialize, Serialize};
+use std::collections::HashSet;
use std::fs::File;
use std::io::{self, Read};
use std::path::PathBuf;
@@ -9,6 +10,7 @@ use thiserror::Error;
use std::convert::TryFrom;
use super::keybinds::Keybinds;
+use super::layout::{RunPlugin, RunPluginOrAlias};
use super::options::Options;
use super::plugins::{PluginAliases, PluginsConfigError};
use super::theme::{Themes, UiConfig};
@@ -29,6 +31,7 @@ pub struct Config {
pub plugins: PluginAliases,
pub ui: UiConfig,
pub env: EnvironmentVariables,
+ pub background_plugins: HashSet<RunPluginOrAlias>,
}
#[derive(Error, Debug)]
@@ -400,7 +403,7 @@ mod config_test {
use crate::data::{InputMode, Palette, PaletteColor, PluginTag};
use crate::input::layout::{RunPlugin, RunPluginLocation};
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 std::collections::{BTreeMap, HashMap};
use std::io::Write;
diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs
index 5a2ac645..a646b386 100644
--- a/zellij-utils/src/input/plugins.rs
+++ b/zellij-utils/src/input/plugins.rs
@@ -35,8 +35,6 @@ impl PluginAliases {
pub struct PluginConfig {
/// Path of the plugin, see resolve_wasm_bytes for resolution semantics
pub path: PathBuf,
- /// Plugin type
- pub run: PluginType,
/// Allow command execution from plugin
pub _allow_exec_host_cmd: bool,
/// Original location of the
@@ -52,7 +50,6 @@ impl PluginConfig {
match &run_plugin.location {
RunPluginLocation::File(path) => Some(PluginConfig {
path: path.clone(),
- run: PluginType::Pane(None),
_allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
location: run_plugin.location.clone(),
userspace_configuration: run_plugin.configuration.clone(),
@@ -69,7 +66,6 @@ impl PluginConfig {
{
Some(PluginConfig {
path: PathBuf::from(&tag),
- run: PluginType::Pane(None),
_allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
location: RunPluginLocation::parse(&format!("zellij:{}", tag), None)
.ok()?,
@@ -82,7 +78,6 @@ impl PluginConfig {
},
RunPluginLocation::Remote(_) => Some(PluginConfig {
path: PathBuf::new(),
- run: PluginType::Pane(None),
_allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
location: run_plugin.location.clone(),
userspace_configuration: run_plugin.configuration.clone(),
@@ -187,41 +182,11 @@ impl PluginConfig {
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 {
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)]
pub enum PluginsConfigError {
#[error("Duplication in plugin tag names is not allowed: '{}'", String::from(.0.clone()))]
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index 3c8aeb71..e57bb828 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -3616,6 +3616,10 @@ impl Config {
let config_plugins = PluginAliases::from_kdl(kdl_plugin_aliases)?;
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") {
let config_ui = UiConfig::from_kdl(&kdl_ui_config)?;
config.ui = config.ui.merge(config_ui);
@@ -3640,6 +3644,9 @@ impl Config {
let plugins = self.plugins.to_kdl(add_comments);
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() {
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 {
pub fn from_kdl(kdl_ui_config: &KdlNode) -> Result<UiConfig, ConfigError> {
let mut ui_config = UiConfig::default();
diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap
index 6cd58d55..a385211a 100644
--- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap
+++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap
@@ -1,6 +1,6 @@
---
source: zellij-utils/src/kdl/mod.rs
-assertion_line: 5060
+assertion_line: 5525
expression: fake_config_stringified
---
keybinds clear-defaults=true {
@@ -237,4 +237,6 @@ plugins {
welcome_screen true
}
}
+load_plugins {
+}
diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap
index c30dca10..3f438da1 100644
--- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap
+++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap
@@ -1,6 +1,6 @@
---
source: zellij-utils/src/kdl/mod.rs
-assertion_line: 5441
+assertion_line: 5537
expression: fake_config_stringified
---
keybinds clear-defaults=true {
@@ -240,6 +240,12 @@ plugins {
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)
// Options:
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
index 45657125..07892df6 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap
@@ -1,6 +1,6 @@
---
source: zellij-utils/src/setup.rs
-assertion_line: 753
+assertion_line: 754
expression: "format!(\"{:#?}\", config)"
---
Config {
@@ -5682,4 +5682,5 @@ Config {
},
},
env: {},
+ background_plugins: {},
}
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
index 95e4ff27..2d4200de 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap
@@ -1,6 +1,6 @@
---
source: zellij-utils/src/setup.rs
-assertion_line: 811
+assertion_line: 812
expression: "format!(\"{:#?}\", config)"
---
Config {
@@ -5686,4 +5686,5 @@ Config {
"LAYOUT_ENV_VAR": "make sure I'm also here",
"MY_ENV_VAR": "from layout",
},
+ background_plugins: {},
}
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap
index 028feefe..b7b66e3a 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap
@@ -1,6 +1,6 @@
---
source: zellij-utils/src/setup.rs
-assertion_line: 853
+assertion_line: 854
expression: "format!(\"{:#?}\", config)"
---
Config {
@@ -231,4 +231,5 @@ Config {
},
},
env: {},
+ background_plugins: {},
}
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
index bfda7107..87ccfb70 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap
@@ -5989,4 +5989,5 @@ Config {
},
},
env: {},
+ background_plugins: {},
}
diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
index 5233e9e4..17f0e6d6 100644
--- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
+++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap
@@ -1,6 +1,6 @@
---
source: zellij-utils/src/setup.rs
-assertion_line: 825
+assertion_line: 826
expression: "format!(\"{:#?}\", config)"
---
Config {
@@ -5682,4 +5682,5 @@ Config {
},
},
env: {},
+ background_plugins: {},
}