From ce8e3995df9e554250543e1a8d90e2e053e3dbec Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Fri, 20 Sep 2024 15:38:20 +0200 Subject: [PATCH] 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 --- src/tests/e2e/cases.rs | 52 +++ src/tests/e2e/remote_runner.rs | 4 + ...load_plugins_in_background_on_startup.snap | 29 ++ .../configs/load_background_plugins.kdl | 392 ++++++++++++++++++ zellij-server/src/lib.rs | 9 +- zellij-server/src/panes/plugin_pane.rs | 8 + zellij-server/src/plugins/mod.rs | 65 ++- zellij-server/src/plugins/plugin_loader.rs | 3 - .../src/plugins/unit/plugin_tests.rs | 12 + zellij-server/src/plugins/zellij_exports.rs | 38 +- zellij-server/src/screen.rs | 90 +++- zellij-server/src/tab/mod.rs | 65 ++- zellij-utils/assets/config/default.kdl | 6 + zellij-utils/src/input/config.rs | 5 +- zellij-utils/src/input/plugins.rs | 35 -- zellij-utils/src/kdl/mod.rs | 104 +++++ ..._config_from_default_assets_to_string.snap | 4 +- ...efault_assets_to_string_with_comments.snap | 8 +- ..._default_config_with_no_cli_arguments.snap | 3 +- ...out_env_vars_override_config_env_vars.snap | 3 +- ...out_keybinds_override_config_keybinds.snap | 3 +- ..._layout_themes_override_config_themes.snap | 1 + ..._ui_config_overrides_config_ui_config.snap | 3 +- 23 files changed, 839 insertions(+), 103 deletions(-) create mode 100644 src/tests/e2e/snapshots/zellij__tests__e2e__cases__load_plugins_in_background_on_startup.snap create mode 100644 src/tests/fixtures/configs/load_background_plugins.kdl 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 +  New Pane  <←↓↑→> Change Focus  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: +// +// 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, 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, + 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, default_mode: InputMode, default_keybinds: Keybinds, + background_plugins: HashSet, + // 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, + 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, ) { let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from(".")); + let initiating_client_id = 1; let (to_server, _server_receiver): ChannelWithContext = 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) -> 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) -> 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), 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 = 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 = 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 = 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; +pub type SuppressedPanes = HashMap)>; // 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)>, // bool => is scrollback editor + suppressed_panes: SuppressedPanes, max_panes: Option, viewport: Rc>, // includes all non-UI panes display_area: Rc>, // 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) { // 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) { + 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, } #[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), // 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, + 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, ConfigError> { + let mut load_plugins: HashSet = 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 { 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: {}, }