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