diff options
| author | Aram Drevekenin <aram@poor.dev> | 2025-12-11 14:25:42 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-12-11 14:25:42 +0100 |
| commit | 693fa082def5ef0fe030cd7fdc3b1df051b35dc6 (patch) | |
| tree | b3f651462bbc1cd2fa5b2d79207343413ef6b83c /zellij-utils/src | |
| parent | fix(zellij-server): unblock building on 32bit targets (#4484) (diff) | |
| download | sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.tar.gz sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.tar.xz sl0ppij-693fa082def5ef0fe030cd7fdc3b1df051b35dc6.zip | |
Command Sequences, Blocking CLI Commands, New Plugin APIs (#4546)
* fix deadlocks
* initial logging to plugin implementation
* only log actions, include keys as Action::Write
* send actions to plugins as a new Event::UserAction
* add Action::Write metadata to plugin API
* run_action plugin command and permission
* block-until-exit/success/failure in the cli
* fix tests
* add NewBlockingPane to the plugin API
* run actions in a different thread so as not to block plugin execution
* add custom context to run_action flow
* fixes for floating pane z_index sorting and unknown protobuf events not crashing plugins
* always render the frame of pinned panes
* macros module
* add macros to config
* KDL macro parsing
* thread macros to the plugin thread
* get/update/delete macros from plugins
* add macros updated event
* add ability to rename macros
* fix deadlock
* fix z_indices with pinned unselectable panes bug
* allow plugins to opt-in to show_cursor
* change show_cursor(bool) to show_cursor(Option<coordinates>)
* test
* upgrade strum to allow proper enum iteration and case insensitive fromstr implementation for Actions
* better cwd tracking for exited command panes
* add initial panes to NewTab Action (no-op for now)
* add initial panes to cli newtab action
* make initial panes work
* add close_on_exit and start_suspended to the cli
* properly convert newtab action (potentially breaking for plugins)
* allow first pane in tab to block
* make distinction between opening panes with the last active client and the current pane in the cli
* improve some apis
* fix fixed size closing pane bug
* fix plugin cursor coordinates
* fix issues with launch-or-focus-plugin having to do with in-place and suppressed panes
* fix: set affected pane in replace pane
* allow setting a pane in place when it is suppressed and being focused (tests failing)
* fix tests
* some fixes for unsuppression of panes
* some fixes for completion_tx in new tab action
* allow replace/suppressed rather than replace/close only
* fixes for inserting suppressed panes without conflicts
* pane focus history and explicit unsuppress plugin api
* fix: prefer explicitly provided pane id in placement in actions that provide it
* fix floating pane coordinate change and z_index
* fix plugin cache dir to persist between sessions
* fix some stack focusing issues when adding panes without a client id
* make sure pinned panes are always above non-pinned floating panes
* allow plugins to copy to clipboard
* initial text styling additions implementations - buggy
* fix dimming issues
* remove extras, stay with just dim and unbold
* unbold and dim as indices
* allow ui components to render success/error colors
* allow plugins to send sigint/sigkill to processes, as well as receive their pid
* ui component fixes and send originating plugin context with run_action
* originating plugin for new tab too
* make plugn show pane command also expand stacked panes
* add sequence plugin
* remove macros
* remove snapshots
* add shortcut for sequence plugin
* fix plugin tests
* plugin tests for new apis
* update snapshots for sequence shortcut
* cleanups and formatting
* Event::CwdChanged
* moar cleanups
* remove some warnings
* remove mroe warnings
* remove dead code
* remove more dead code
* fix minor bugs in sequence plugin
* update sequence help and format codebase
* fix conditional blocking cli commands
* fix initial-command new-tab cli flag
* add client_ids to CwdChanged Event
* last fixes
* add pr
Diffstat (limited to 'zellij-utils/src')
32 files changed, 3131 insertions, 169 deletions
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 0fd52473..e59ba71f 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -1,4 +1,4 @@ -use crate::data::{Direction, InputMode, Resize}; +use crate::data::{Direction, InputMode, Resize, UnblockCondition}; use crate::setup::Setup; use crate::{ consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV}, @@ -388,9 +388,49 @@ pub enum Sessions { takes_value(false) )] stacked: bool, - /// Whether to block until this command has finished + /// Block until the command has finished and its pane has been closed #[clap(long, value_parser, default_value("false"), takes_value(false))] blocking: bool, + + /// Block until the command exits successfully (exit status 0) OR its pane has been closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + conflicts_with("blocking"), + conflicts_with("block-until-exit-failure"), + conflicts_with("block-until-exit") + )] + block_until_exit_success: bool, + + /// Block until the command exits with failure (non-zero exit status) OR its pane has been + /// closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + conflicts_with("blocking"), + conflicts_with("block-until-exit-success"), + conflicts_with("block-until-exit") + )] + block_until_exit_failure: bool, + + /// Block until the command exits (regardless of exit status) OR its pane has been closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + conflicts_with("blocking"), + conflicts_with("block-until-exit-success"), + conflicts_with("block-until-exit-failure") + )] + block_until_exit: bool, + /// if set, will open the pane near the current one rather than following the user's focus + #[clap(long)] + near_current_pane: bool, }, /// Load a plugin #[clap(visible_alias = "p")] @@ -484,6 +524,9 @@ pub enum Sessions { /// Whether to pin a floating pane so that it is always on top #[clap(long, requires("floating"))] pinned: Option<bool>, + /// if set, will open the pane near the current one rather than following the user's focus + #[clap(long)] + near_current_pane: bool, }, ConvertConfig { old_config_file: PathBuf, @@ -691,6 +734,14 @@ pub enum CliAction { stacked: bool, #[clap(short, long)] blocking: bool, + + // TODO: clean this up + #[clap(skip)] + unblock_condition: Option<UnblockCondition>, + + /// if set, will open the pane near the current one rather than following the user's focus + #[clap(long)] + near_current_pane: bool, }, /// Open the specified file in a new zellij pane with your default EDITOR Edit { @@ -738,6 +789,9 @@ pub enum CliAction { /// Whether to pin a floating pane so that it is always on top #[clap(long, requires("floating"))] pinned: Option<bool>, + /// if set, will open the pane near the current one rather than following the user's focus + #[clap(long)] + near_current_pane: bool, }, /// Switch input mode of all connected clients [locked|pane|tab|resize|move|search|session] SwitchMode { @@ -795,6 +849,76 @@ pub enum CliAction { /// Change the working directory of the new tab #[clap(short, long, value_parser)] cwd: Option<PathBuf>, + + /// Optional initial command to run in the new tab + #[clap( + value_parser, + conflicts_with("initial-plugin"), + multiple_values(true), + takes_value(true), + last(true) + )] + initial_command: Vec<String>, + + /// Initial plugin to load in the new tab + #[clap(long, value_parser, conflicts_with("initial-command"))] + initial_plugin: Option<String>, + + /// Close the pane immediately when its command exits + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + requires("initial-command") + )] + close_on_exit: bool, + + /// Start the command suspended, only running it after you first press ENTER + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + requires("initial-command") + )] + start_suspended: bool, + + /// Block until the command exits successfully (exit status 0) OR its pane has been closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + requires("initial-command"), + conflicts_with("block-until-exit-failure"), + conflicts_with("block-until-exit") + )] + block_until_exit_success: bool, + + /// Block until the command exits with failure (non-zero exit status) OR its pane has been closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + requires("initial-command"), + conflicts_with("block-until-exit-success"), + conflicts_with("block-until-exit") + )] + block_until_exit_failure: bool, + + /// Block until the command exits (regardless of exit status) OR its pane has been closed + #[clap( + long, + value_parser, + default_value("false"), + takes_value(false), + requires("initial-command"), + conflicts_with("block-until-exit-success"), + conflicts_with("block-until-exit-failure") + )] + block_until_exit: bool, }, /// Move the focused tab in the specified direction. [right|left] MoveTab { diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto index 254a2a75..e4561989 100644 --- a/zellij-utils/src/client_server_contract/common_types.proto +++ b/zellij-utils/src/client_server_contract/common_types.proto @@ -333,6 +333,7 @@ message NewPaneAction { optional Direction direction = 1; optional string pane_name = 2; bool start_suppressed = 3; + bool near_current_pane = 4; } message EditFileAction { @@ -342,34 +343,43 @@ message EditFileAction { bool in_place = 4; bool start_suppressed = 5; optional FloatingPaneCoordinates coordinates = 6; + bool near_current_pane = 7; } message NewFloatingPaneAction { optional RunCommandAction command = 1; optional string pane_name = 2; optional FloatingPaneCoordinates coordinates = 3; + bool near_current_pane = 7; } message NewTiledPaneAction { optional Direction direction = 1; optional RunCommandAction command = 2; optional string pane_name = 3; + bool near_current_pane = 7; } message NewInPlacePaneAction { optional RunCommandAction command = 1; optional string pane_name = 2; + bool near_current_pane = 3; + optional PaneId pane_id_to_replace = 4; + bool close_replace_pane = 5; } message NewStackedPaneAction { optional RunCommandAction command = 1; optional string pane_name = 2; + bool near_current_pane = 3; } message NewBlockingPaneAction { NewPanePlacement placement = 1; optional string pane_name = 2; optional RunCommandAction command = 3; + optional UnblockCondition unblock_condition = 4; + bool near_current_pane = 5; } message PaneNameInputAction { @@ -384,6 +394,8 @@ message NewTabAction { optional string tab_name = 5; bool should_change_focus_to_new_tab = 6; optional string cwd = 7; + repeated CommandOrPlugin initial_panes = 8; + optional UnblockCondition first_pane_unblock_condition = 9; } message GoToTabAction { @@ -405,6 +417,7 @@ message MoveTabAction { message RunAction { RunCommandAction command = 1; + bool near_current_pane = 2; } message LaunchOrFocusPluginAction { @@ -479,11 +492,13 @@ message ClosePluginPaneAction { message FocusTerminalPaneWithIdAction { uint32 pane_id = 1; bool should_float_if_hidden = 2; + bool should_be_in_place_if_hidden = 3; } message FocusPluginPaneWithIdAction { uint32 pane_id = 1; bool should_float_if_hidden = 2; + bool should_be_in_place_if_hidden = 3; } message RenameTerminalPaneAction { @@ -571,6 +586,13 @@ enum Direction { DIRECTION_DOWN = 4; } +enum UnblockCondition { + UNBLOCK_CONDITION_UNSPECIFIED = 0; + UNBLOCK_CONDITION_ON_EXIT_SUCCESS = 1; + UNBLOCK_CONDITION_ON_EXIT_FAILURE = 2; + UNBLOCK_CONDITION_ON_ANY_EXIT = 3; +} + enum ResizeType { RESIZE_TYPE_UNSPECIFIED = 0; RESIZE_TYPE_INCREASE = 1; @@ -761,6 +783,13 @@ message PluginAlias { optional RunPlugin run_plugin = 4; } +message CommandOrPlugin { + oneof command_or_plugin_type { + RunCommandAction command = 1; + RunPluginOrAlias plugin = 2; + } +} + message MouseEvent { MouseEventType event_type = 1; bool left = 2; diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs index 4f5b729d..e5b93e1a 100644 --- a/zellij-utils/src/consts.rs +++ b/zellij-utils/src/consts.rs @@ -138,6 +138,7 @@ mod not_wasm { add_plugin!(assets, "about.wasm"); add_plugin!(assets, "share.wasm"); add_plugin!(assets, "multiple-select.wasm"); + add_plugin!(assets, "sequence.wasm"); assets }; } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 7f700d4d..62571fd4 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -1,8 +1,8 @@ use crate::home::default_layout_dir; -use crate::input::actions::Action; +use crate::input::actions::{Action, RunCommandAction}; use crate::input::config::ConversionError; use crate::input::keybinds::Keybinds; -use crate::input::layout::{RunPlugin, SplitSize}; +use crate::input::layout::{RunPlugin, RunPluginOrAlias, SplitSize}; use crate::pane_size::PaneGeom; use crate::position::Position; use crate::shared::{colors as default_colors, eightbit_to_rgb}; @@ -15,7 +15,7 @@ use std::net::IpAddr; use std::path::{Path, PathBuf}; use std::str::{self, FromStr}; use std::time::Duration; -use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString, ToString}; +use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString}; use unicode_width::UnicodeWidthChar; #[cfg(not(target_family = "wasm"))] @@ -26,6 +26,39 @@ use termwiz::{ pub type ClientId = u16; // TODO: merge with crate type? +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum UnblockCondition { + /// Unblock only when exit status is 0 (success) + OnExitSuccess, + /// Unblock only when exit status is non-zero (failure) + OnExitFailure, + /// Unblock on any exit (success or failure) + OnAnyExit, +} + +impl UnblockCondition { + /// Check if the condition is met for the given exit status + pub fn is_met(&self, exit_status: i32) -> bool { + match self { + UnblockCondition::OnExitSuccess => exit_status == 0, + UnblockCondition::OnExitFailure => exit_status != 0, + UnblockCondition::OnAnyExit => true, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum CommandOrPlugin { + Command(RunCommandAction), + Plugin(RunPluginOrAlias), +} + +impl CommandOrPlugin { + pub fn new_command(command: Vec<String>) -> Self { + CommandOrPlugin::Command(RunCommandAction::new(command)) + } +} + pub fn client_id_to_colors( client_id: ClientId, colors: MultiplayerColors, @@ -252,7 +285,7 @@ impl FromStr for BareKey { } #[derive( - Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, ToString, + Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, Display, )] pub enum KeyModifier { Ctrl, @@ -589,6 +622,12 @@ pub enum Direction { Down, } +impl Default for Direction { + fn default() -> Self { + Direction::Left + } +} + impl Direction { pub fn invert(&self) -> Direction { match *self { @@ -642,6 +681,12 @@ pub enum Resize { Decrease, } +impl Default for Resize { + fn default() -> Self { + Resize::Increase + } +} + impl Resize { pub fn invert(&self) -> Self { match self { @@ -877,7 +922,7 @@ impl From<Metadata> for FileMetadata { /// These events can be subscribed to with subscribe method exported by `zellij-tile`. /// Once subscribed to, they will trigger the `update` method of the `ZellijPlugin` trait. -#[derive(Debug, Clone, PartialEq, EnumDiscriminants, ToString, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, EnumDiscriminants, Display, Serialize, Deserialize)] #[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))] #[strum_discriminants(name(EventType))] #[non_exhaustive] @@ -946,10 +991,14 @@ pub enum Event { FailedToStartWebServer(String), BeforeClose, InterceptedKeyPress(KeyWithModifier), + /// An action was performed by the user (requires InterceptInput permission) + UserAction(Action, ClientId, Option<u32>, Option<ClientId>), // Action, client_id, terminal_id, cli_client_id PaneRenderReport(HashMap<PaneId, PaneContents>), + ActionComplete(Action, Option<PaneId>, BTreeMap<String, String>), // Action, pane_id, context + CwdChanged(PaneId, PathBuf, Vec<ClientId>), // pane_id, cwd, focused_client_ids } -#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants, ToString, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants, Display, Serialize, Deserialize)] pub enum WebServerStatus { Online(String), // String -> base url Offline, @@ -964,7 +1013,7 @@ pub enum WebServerStatus { Copy, Clone, EnumDiscriminants, - ToString, + Display, Serialize, Deserialize, PartialOrd, @@ -988,6 +1037,8 @@ pub enum Permission { StartWebServer, InterceptInput, ReadPaneContents, + RunActionsAsUser, + WriteToClipboard, } impl PermissionType { @@ -1017,6 +1068,8 @@ impl PermissionType { PermissionType::ReadPaneContents => { "Read pane contents (viewport and selection)".to_owned() }, + PermissionType::RunActionsAsUser => "Execute actions as the user".to_owned(), + PermissionType::WriteToClipboard => "Write to clipboard".to_owned(), } } } @@ -1643,6 +1696,7 @@ pub struct SessionInfo { pub web_clients_allowed: bool, pub web_client_count: usize, pub tab_history: BTreeMap<ClientId, Vec<usize>>, + pub pane_history: BTreeMap<ClientId, Vec<PaneId>>, } #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] @@ -2053,6 +2107,12 @@ pub enum PaneScrollbackResponse { Err(String), } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum GetPanePidResponse { + Ok(i32), + Err(String), +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SelectedText { pub start: Position, @@ -2226,6 +2286,12 @@ pub enum PaneId { Plugin(u32), } +impl Default for PaneId { + fn default() -> Self { + PaneId::Terminal(0) + } +} + impl FromStr for PaneId { type Err = Box<dyn std::error::Error>; fn from_str(stringified_pane_id: &str) -> Result<Self, Self::Err> { @@ -2660,13 +2726,14 @@ impl NewPanePlacement { type Context = BTreeMap<String, String>; -#[derive(Debug, Clone, EnumDiscriminants, ToString)] +#[derive(Debug, Clone, EnumDiscriminants, Display)] #[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))] #[strum_discriminants(name(CommandType))] pub enum PluginCommand { Subscribe(HashSet<EventType>), Unsubscribe(HashSet<EventType>), SetSelectable(bool), + ShowCursor(Option<(usize, usize)>), GetPluginIds, GetZellijVersion, OpenFile(FileToOpen, Context), @@ -2723,16 +2790,16 @@ pub enum PluginCommand { NextSwapLayout, GoToTabName(String), FocusOrCreateTab(String), - GoToTab(u32), // tab index - StartOrReloadPlugin(String), // plugin url (eg. file:/path/to/plugin.wasm) - CloseTerminalPane(u32), // terminal pane id - ClosePluginPane(u32), // plugin pane id - FocusTerminalPane(u32, bool), // terminal pane id, should_float_if_hidden - FocusPluginPane(u32, bool), // plugin pane id, should_float_if_hidden - RenameTerminalPane(u32, String), // terminal pane id, new name - RenamePluginPane(u32, String), // plugin pane id, new name - RenameTab(u32, String), // tab index, new name - ReportPanic(String), // stringified panic + GoToTab(u32), // tab index + StartOrReloadPlugin(String), // plugin url (eg. file:/path/to/plugin.wasm) + CloseTerminalPane(u32), // terminal pane id + ClosePluginPane(u32), // plugin pane id + FocusTerminalPane(u32, bool, bool), // terminal pane id, should_float_if_hidden, should_be_in_place_if_hidden + FocusPluginPane(u32, bool, bool), // plugin pane id, should_float_if_hidden, should_be_in_place_if_hidden + RenameTerminalPane(u32, String), // terminal pane id, new name + RenamePluginPane(u32, String), // plugin pane id, new name + RenameTab(u32, String), // tab index, new name + ReportPanic(String), // stringified panic RequestPluginPermissions(Vec<PermissionType>), SwitchSession(ConnectToSession), DeleteDeadSession(String), // String -> session name @@ -2768,7 +2835,7 @@ pub enum PluginCommand { Reconfigure(String, bool), // String -> stringified configuration, bool -> save configuration // file to disk HidePaneWithId(PaneId), - ShowPaneWithId(PaneId, bool), // bool -> should_float_if_hidden + ShowPaneWithId(PaneId, bool, bool), // bools -> should_float_if_hidden, should_focus_pane OpenCommandPaneBackground(CommandToRun, Context), RerunCommandPane(u32), // u32 - terminal pane id ResizePaneIdWithDirection(ResizeStrategy, PaneId), @@ -2779,6 +2846,11 @@ pub enum PluginCommand { }, WriteToPaneId(Vec<u8>, PaneId), WriteCharsToPaneId(String, PaneId), + SendSigintToPaneId(PaneId), + SendSigkillToPaneId(PaneId), + GetPanePid { + pane_id: PaneId, + }, MovePaneWithPaneId(PaneId), MovePaneWithPaneIdInDirection(PaneId, Direction), ClearScreenForPaneId(PaneId), @@ -2844,5 +2916,8 @@ pub enum PluginCommand { RenameWebLoginToken(String, String), // (original_name, new_name) InterceptKeyPresses, ClearKeyPressesIntercepts, - ReplacePaneWithExistingPane(PaneId, PaneId), // (pane id to replace, pane id of existing) + ReplacePaneWithExistingPane(PaneId, PaneId, bool), // (pane id to replace, pane id of existing, + // suppress_replaced_pane) + RunAction(Action, BTreeMap<String, String>), + CopyToClipboard(String), // text to copy } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index bcd05987..74c40534 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -277,6 +277,7 @@ pub enum ScreenContext { ToggleActiveTerminalFullscreen, TogglePaneFrames, SetSelectable, + ShowPluginCursor, SetInvisibleBorders, SetFixedHeight, SetFixedWidth, @@ -313,10 +314,6 @@ pub enum ScreenContext { ToggleTab, AddClient, RemoveClient, - AddOverlay, - RemoveOverlay, - ConfirmPrompt, - DenyPrompt, UpdateSearch, SearchDown, SearchUp, @@ -339,6 +336,8 @@ pub enum ScreenContext { LaunchOrFocusPlugin, LaunchPlugin, SuppressPane, + UnsuppressPane, + UnsuppressOrExpandPane, FocusPaneWithId, RenamePane, RenameTab, @@ -358,6 +357,7 @@ pub enum ScreenContext { ResizePaneWithId, EditScrollbackForPaneWithId, WriteToPaneId, + CopyTextToClipboard, MovePaneWithPaneId, MovePaneWithPaneIdInDirection, ClearScreenForPaneId, @@ -418,6 +418,10 @@ pub enum PtyContext { Reconfigure, ListClientsToPlugin, ReportPluginCwd, + SendSigintToPaneId, + SendSigkillToPaneId, + GetPanePid, + UpdateAndReportCwds, Exit, } @@ -460,6 +464,7 @@ pub enum PluginContext { WebServerStarted, FailedToStartWebServer, PaneRenderReport, + UserInput, } /// Stack call representations corresponding to the different types of [`ClientInstruction`]s. diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index d623197b..92d697a9 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -1,12 +1,15 @@ //! Definition of the actions that can be bound to keys. -use super::command::{OpenFilePayload, RunCommandAction}; +pub use super::command::{OpenFilePayload, RunCommandAction}; use super::layout::{ FloatingPaneLayout, Layout, PluginAlias, RunPlugin, RunPluginLocation, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }; use crate::cli::CliAction; -use crate::data::{Direction, KeyWithModifier, LayoutInfo, NewPanePlacement, PaneId, Resize}; +use crate::data::{ + CommandOrPlugin, Direction, KeyWithModifier, LayoutInfo, NewPanePlacement, OriginatingPlugin, + PaneId, Resize, UnblockCondition, +}; use crate::data::{FloatingPaneCoordinates, InputMode}; use crate::home::{find_default_config_dir, get_layout_dir}; use crate::input::config::{Config, ConfigError, KdlError}; @@ -99,7 +102,18 @@ impl FromStr for SearchOption { // They might need to be adjusted in the default config // as well `../../assets/config/default.yaml` /// Actions that can be bound to keys. -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, strum_macros::Display)] +#[derive( + Clone, + Debug, + PartialEq, + Eq, + Deserialize, + Serialize, + strum_macros::Display, + strum_macros::EnumString, + strum_macros::EnumIter, +)] +#[strum(ascii_case_insensitive)] pub enum Action { /// Quit Zellij. Quit, @@ -194,6 +208,8 @@ pub enum Action { placement: NewPanePlacement, pane_name: Option<String>, command: Option<RunCommandAction>, + unblock_condition: Option<UnblockCondition>, + near_current_pane: bool, }, /// Open the file in a new pane using the default editor EditFile { @@ -203,27 +219,34 @@ pub enum Action { in_place: bool, start_suppressed: bool, coordinates: Option<FloatingPaneCoordinates>, + near_current_pane: bool, }, /// Open a new floating pane NewFloatingPane { command: Option<RunCommandAction>, pane_name: Option<String>, coordinates: Option<FloatingPaneCoordinates>, + near_current_pane: bool, }, /// Open a new tiled (embedded, non-floating) pane NewTiledPane { direction: Option<Direction>, command: Option<RunCommandAction>, pane_name: Option<String>, + near_current_pane: bool, }, /// Open a new pane in place of the focused one, suppressing it instead NewInPlacePane { command: Option<RunCommandAction>, pane_name: Option<String>, + near_current_pane: bool, + pane_id_to_replace: Option<PaneId>, + close_replace_pane: bool, }, NewStackedPane { command: Option<RunCommandAction>, pane_name: Option<String>, + near_current_pane: bool, }, /// Embed focused pane in tab if floating or float focused pane if embedded TogglePaneEmbedOrFloating, @@ -244,6 +267,8 @@ pub enum Action { tab_name: Option<String>, should_change_focus_to_new_tab: bool, cwd: Option<PathBuf>, + initial_panes: Option<Vec<CommandOrPlugin>>, + first_pane_unblock_condition: Option<UnblockCondition>, }, /// Do nothing. NoOp, @@ -271,6 +296,7 @@ pub enum Action { /// Run specified command in new pane. Run { command: RunCommandAction, + near_current_pane: bool, }, /// Detach session and exit Detach, @@ -356,10 +382,12 @@ pub enum Action { FocusTerminalPaneWithId { pane_id: u32, should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, }, FocusPluginPaneWithId { pane_id: u32, should_float_if_hidden: bool, + should_be_in_place_if_hidden: bool, }, RenameTerminalPane { pane_id: u32, @@ -420,6 +448,24 @@ pub enum Action { ToggleGroupMarking, } +impl Default for Action { + fn default() -> Self { + Action::NoOp + } +} + +impl Default for SearchDirection { + fn default() -> Self { + SearchDirection::Down + } +} + +impl Default for SearchOption { + fn default() -> Self { + SearchOption::CaseSensitivity + } +} + impl Action { /// Checks that two Action are match except their mutable attributes. pub fn shallow_eq(&self, other_action: &Action) -> bool { @@ -492,6 +538,8 @@ impl Action { pinned, stacked, blocking, + unblock_condition, + near_current_pane, } => { let current_dir = get_current_dir(); // cwd should only be specified in a plugin alias if it was explicitly given to us, @@ -500,7 +548,7 @@ impl Action { let cwd = cwd .map(|cwd| current_dir.join(cwd)) .or_else(|| Some(current_dir.clone())); - if blocking { + if blocking || unblock_condition.is_some() { // For blocking panes, we don't support plugins if plugin.is_some() { return Err("Blocking panes do not support plugin variants".to_string()); @@ -543,6 +591,8 @@ impl Action { placement, pane_name: name, command, + unblock_condition, + near_current_pane, }]) } else if let Some(plugin) = plugin { let plugin = match RunPluginLocation::parse(&plugin, cwd.clone()) { @@ -614,22 +664,28 @@ impl Action { command: Some(run_command_action), pane_name: name, coordinates: FloatingPaneCoordinates::new(x, y, width, height, pinned), + near_current_pane, }]) } else if in_place { Ok(vec![Action::NewInPlacePane { command: Some(run_command_action), pane_name: name, + near_current_pane, + pane_id_to_replace: None, // TODO: support this + close_replace_pane: false, }]) } else if stacked { Ok(vec![Action::NewStackedPane { command: Some(run_command_action), pane_name: name, + near_current_pane, }]) } else { Ok(vec![Action::NewTiledPane { direction, command: Some(run_command_action), pane_name: name, + near_current_pane, }]) } } else { @@ -638,22 +694,28 @@ impl Action { command: None, pane_name: name, coordinates: FloatingPaneCoordinates::new(x, y, width, height, pinned), + near_current_pane, }]) } else if in_place { Ok(vec![Action::NewInPlacePane { command: None, pane_name: name, + near_current_pane, + pane_id_to_replace: None, // TODO: support this + close_replace_pane: false, }]) } else if stacked { Ok(vec![Action::NewStackedPane { command: None, pane_name: name, + near_current_pane, }]) } else { Ok(vec![Action::NewTiledPane { direction, command: None, pane_name: name, + near_current_pane, }]) } } @@ -670,6 +732,7 @@ impl Action { width, height, pinned, + near_current_pane, } => { let mut file = file; let current_dir = get_current_dir(); @@ -689,6 +752,7 @@ impl Action { in_place, start_suppressed, coordinates: FloatingPaneCoordinates::new(x, y, width, height, pinned), + near_current_pane, }]) }, CliAction::SwitchMode { input_mode } => Ok(vec![Action::SwitchToMode { input_mode }]), @@ -721,11 +785,68 @@ impl Action { layout, layout_dir, cwd, + initial_command, + initial_plugin, + close_on_exit, + start_suspended, + block_until_exit_success, + block_until_exit_failure, + block_until_exit, } => { let current_dir = get_current_dir(); let cwd = cwd .map(|cwd| current_dir.join(cwd)) - .or_else(|| Some(current_dir)); + .or_else(|| Some(current_dir.clone())); + + // Map CLI flags to UnblockCondition + let first_pane_unblock_condition = if block_until_exit_success { + Some(UnblockCondition::OnExitSuccess) + } else if block_until_exit_failure { + Some(UnblockCondition::OnExitFailure) + } else if block_until_exit { + Some(UnblockCondition::OnAnyExit) + } else { + None + }; + + // Parse initial_panes from initial_command or initial_plugin + let initial_panes = if let Some(plugin_url) = initial_plugin { + let plugin = match RunPluginLocation::parse(&plugin_url, cwd.clone()) { + Ok(location) => RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location, + configuration: Default::default(), + initial_cwd: cwd.clone(), + }), + Err(_) => { + let mut plugin_alias = + PluginAlias::new(&plugin_url, &None, cwd.clone()); + plugin_alias.set_caller_cwd_if_not_set(Some(current_dir.clone())); + RunPluginOrAlias::Alias(plugin_alias) + }, + }; + Some(vec![CommandOrPlugin::Plugin(plugin)]) + } else if !initial_command.is_empty() { + let mut command: Vec<String> = initial_command.clone(); + let (command, args) = ( + PathBuf::from(command.remove(0)), + command.into_iter().collect(), + ); + let hold_on_close = !close_on_exit; + let hold_on_start = start_suspended; + let run_command_action = RunCommandAction { + command, + args, + cwd: cwd.clone(), + direction: None, + hold_on_close, + hold_on_start, + ..Default::default() + }; + Some(vec![CommandOrPlugin::Command(run_command_action)]) + } else { + None + }; if let Some(layout_path) = layout { let layout_dir = layout_dir .or_else(|| config.and_then(|c| c.options.layout_dir)) @@ -813,6 +934,8 @@ impl Action { tab_name: name, should_change_focus_to_new_tab, cwd: None, // the cwd is done through the layout + initial_panes: initial_panes.clone(), + first_pane_unblock_condition, }); } Ok(new_tab_actions) @@ -829,6 +952,8 @@ impl Action { tab_name: name, should_change_focus_to_new_tab, cwd: None, // the cwd is done through the layout + initial_panes, + first_pane_unblock_condition, }]) } } else { @@ -841,6 +966,8 @@ impl Action { tab_name: name, should_change_focus_to_new_tab, cwd, + initial_panes, + first_pane_unblock_condition, }]) } }, @@ -1047,6 +1174,38 @@ impl Action { }, } } + pub fn populate_originating_plugin(&mut self, originating_plugin: OriginatingPlugin) { + match self { + Action::NewBlockingPane { command, .. } + | Action::NewFloatingPane { command, .. } + | Action::NewTiledPane { command, .. } + | Action::NewInPlacePane { command, .. } + | Action::NewStackedPane { command, .. } => { + command + .as_mut() + .map(|c| c.populate_originating_plugin(originating_plugin)); + }, + Action::Run { command, .. } => { + command.populate_originating_plugin(originating_plugin); + }, + Action::EditFile { payload, .. } => { + payload.originating_plugin = Some(originating_plugin); + }, + Action::NewTab { initial_panes, .. } => { + if let Some(initial_panes) = initial_panes.as_mut() { + for pane in initial_panes.iter_mut() { + match pane { + CommandOrPlugin::Command(run_command) => { + run_command.populate_originating_plugin(originating_plugin.clone()); + }, + _ => {}, + } + } + } + }, + _ => {}, + } + } pub fn launches_plugin(&self, plugin_url: &str) -> bool { match self { Action::LaunchPlugin { plugin, .. } => &plugin.location_string() == plugin_url, diff --git a/zellij-utils/src/input/command.rs b/zellij-utils/src/input/command.rs index 0169f799..11a34f59 100644 --- a/zellij-utils/src/input/command.rs +++ b/zellij-utils/src/input/command.rs @@ -30,6 +30,17 @@ pub struct OpenFilePayload { pub originating_plugin: Option<OriginatingPlugin>, } +impl Default for OpenFilePayload { + fn default() -> Self { + OpenFilePayload { + path: PathBuf::new(), + line_number: None, + cwd: None, + originating_plugin: None, + } + } +} + impl OpenFilePayload { pub fn new(path: PathBuf, line_number: Option<usize>, cwd: Option<PathBuf>) -> Self { OpenFilePayload { @@ -129,6 +140,23 @@ impl From<RunCommand> for RunCommandAction { } } +impl RunCommandAction { + pub fn new(mut command: Vec<String>) -> Self { + if command.is_empty() { + Default::default() + } else { + RunCommandAction { + command: PathBuf::from(command.remove(0)), + args: command, + ..Default::default() + } + } + } + pub fn populate_originating_plugin(&mut self, originating_plugin: OriginatingPlugin) { + self.originating_plugin = Some(originating_plugin); + } +} + impl RunCommand { pub fn new(command: PathBuf) -> Self { RunCommand { diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index de555a00..d925f7c5 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -995,6 +995,44 @@ impl TiledPaneLayout { } run_instructions } + pub fn replace_next_empty_slot_with_run(&mut self, run_to_insert: Run) -> bool { + // Replaces the first empty slot (None or Run::Cwd) with the given Run instruction. + // Returns true if a replacement was made, false if no empty slot was found. + // Traversal order matches extract_run_instructions (breadth-first). + + if self.children.is_empty() { + // This is a leaf node - check if it's an empty slot + match &self.run { + None | Some(Run::Cwd(_)) => { + self.run = Some(run_to_insert); + return true; + }, + _ => return false, + } + } + + // Check first child of each child (breadth-first first level) + for child in self.children.iter_mut() { + if child.children.is_empty() { + match &child.run { + None | Some(Run::Cwd(_)) => { + child.run = Some(run_to_insert); + return true; + }, + _ => {}, + } + } + } + + // Recursively check deeper levels (breadth-first continuation) + for child in self.children.iter_mut() { + if child.replace_next_empty_slot_with_run(run_to_insert.clone()) { + return true; + } + } + + false + } pub fn ignore_run_instruction(&mut self, run_instruction: Option<Run>) { self.run_instructions_to_ignore.push(run_instruction); } diff --git a/zellij-utils/src/input/mouse.rs b/zellij-utils/src/input/mouse.rs index d920495d..0977d5e7 100644 --- a/zellij-utils/src/input/mouse.rs +++ b/zellij-utils/src/input/mouse.rs @@ -239,3 +239,9 @@ impl MouseEvent { event } } + +impl Default for MouseEvent { + fn default() -> Self { + MouseEvent::new() + } +} diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs index c4755205..892c7a66 100644 --- a/zellij-utils/src/input/plugins.rs +++ b/zellij-utils/src/input/plugins.rs @@ -67,6 +67,7 @@ impl PluginConfig { || tag == "about" || tag == "share" || tag == "multiple-select" + || tag == "sequence" { Some(PluginConfig { path: PathBuf::from(&tag), diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index 53dd4d9f..96e2c747 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -251,18 +251,14 @@ impl<T: Serialize> IpcSenderWithContext<T> { pub fn send_client_msg(&mut self, msg: ClientToServerMsg) -> Result<()> { let proto_msg: ProtoClientToServerMsg = msg.into(); write_protobuf_message(&mut self.sender, &proto_msg)?; - if let Err(e) = self.sender.flush() { - log::error!("Failed to flush ipc sender: {}", e); - } + let _ = self.sender.flush(); Ok(()) } pub fn send_server_msg(&mut self, msg: ServerToClientMsg) -> Result<()> { let proto_msg: ProtoServerToClientMsg = msg.into(); write_protobuf_message(&mut self.sender, &proto_msg)?; - if let Err(e) = self.sender.flush() { - log::error!("Failed to flush ipc sender: {}", e); - } + let _ = self.sender.flush(); Ok(()) } @@ -370,9 +366,7 @@ pub fn send_protobuf_client_to_server( ) -> Result<()> { let proto_msg: ProtoClientToServerMsg = msg.into(); write_protobuf_message(&mut sender.sender, &proto_msg)?; - if let Err(e) = sender.sender.flush() { - log::error!("Failed to flush ipc sender: {}", e); - } + let _ = sender.sender.flush(); Ok(()) } @@ -382,9 +376,7 @@ pub fn send_protobuf_server_to_client( ) -> Result<()> { let proto_msg: ProtoServerToClientMsg = msg.into(); write_protobuf_message(&mut sender.sender, &proto_msg)?; - if let Err(e) = sender.sender.flush() { - log::error!("Failed to flush ipc sender: {}", e); - } + let _ = sender.sender.flush(); Ok(()) } diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs index ab06faae..f7ab632f 100644 --- a/zellij-utils/src/ipc/protobuf_conversion.rs +++ b/zellij-utils/src/ipc/protobuf_conversion.rs @@ -858,6 +858,7 @@ impl From<crate::input::actions::Action> direction: direction.map(|d| direction_to_proto_i32(d)), pane_name, start_suppressed, + near_current_pane: false, }), crate::input::actions::Action::EditFile { payload, @@ -866,6 +867,7 @@ impl From<crate::input::actions::Action> in_place, start_suppressed, coordinates, + near_current_pane, } => ActionType::EditFile(EditFileAction { payload: Some(payload.into()), direction: direction.map(|d| direction_to_proto_i32(d)), @@ -873,45 +875,64 @@ impl From<crate::input::actions::Action> in_place, start_suppressed, coordinates: coordinates.map(|c| c.into()), + near_current_pane, }), crate::input::actions::Action::NewFloatingPane { command, pane_name, coordinates, + near_current_pane, } => ActionType::NewFloatingPane(NewFloatingPaneAction { command: command.map(|c| c.into()), pane_name, coordinates: coordinates.map(|c| c.into()), + near_current_pane, }), crate::input::actions::Action::NewTiledPane { direction, command, pane_name, + near_current_pane, } => ActionType::NewTiledPane(NewTiledPaneAction { direction: direction.map(|d| direction_to_proto_i32(d)), command: command.map(|c| c.into()), pane_name, + near_current_pane, + }), + crate::input::actions::Action::NewInPlacePane { + command, + pane_name, + near_current_pane, + pane_id_to_replace, + close_replace_pane, + } => ActionType::NewInPlacePane(NewInPlacePaneAction { + command: command.map(|c| c.into()), + pane_name, + near_current_pane, + pane_id_to_replace: pane_id_to_replace.and_then(|p| p.try_into().ok()), + close_replace_pane, + }), + crate::input::actions::Action::NewStackedPane { + command, + pane_name, + near_current_pane, + } => ActionType::NewStackedPane(NewStackedPaneAction { + command: command.map(|c| c.into()), + pane_name, + near_current_pane, }), - crate::input::actions::Action::NewInPlacePane { command, pane_name } => { - ActionType::NewInPlacePane(NewInPlacePaneAction { - command: command.map(|c| c.into()), - pane_name, - }) - }, - crate::input::actions::Action::NewStackedPane { command, pane_name } => { - ActionType::NewStackedPane(NewStackedPaneAction { - command: command.map(|c| c.into()), - pane_name, - }) - }, crate::input::actions::Action::NewBlockingPane { placement, pane_name, command, + unblock_condition, + near_current_pane, } => ActionType::NewBlockingPane(NewBlockingPaneAction { placement: Some(placement.into()), pane_name, command: command.map(|c| c.into()), + unblock_condition: unblock_condition.map(|c| unblock_condition_to_proto_i32(c)), + near_current_pane, }), crate::input::actions::Action::TogglePaneEmbedOrFloating => { ActionType::TogglePaneEmbedOrFloating(TogglePaneEmbedOrFloatingAction {}) @@ -938,6 +959,8 @@ impl From<crate::input::actions::Action> tab_name, should_change_focus_to_new_tab, cwd, + initial_panes, + first_pane_unblock_condition, } => ActionType::NewTab(NewTabAction { tiled_layout: tiled_layout.map(|l| l.into()), floating_layouts: floating_layouts.into_iter().map(|l| l.into()).collect(), @@ -950,6 +973,11 @@ impl From<crate::input::actions::Action> tab_name, should_change_focus_to_new_tab, cwd: cwd.map(|p| p.to_string_lossy().to_string()), + initial_panes: initial_panes + .map(|panes| panes.into_iter().map(|p| p.into()).collect()) + .unwrap_or_default(), + first_pane_unblock_condition: first_pane_unblock_condition + .map(|c| unblock_condition_to_proto_i32(c)), }), crate::input::actions::Action::NoOp => ActionType::NoOp(NoOpAction {}), crate::input::actions::Action::GoToNextTab => { @@ -979,8 +1007,12 @@ impl From<crate::input::actions::Action> direction: direction_to_proto_i32(direction), }) }, - crate::input::actions::Action::Run { command } => ActionType::Run(RunAction { + crate::input::actions::Action::Run { + command, + near_current_pane, + } => ActionType::Run(RunAction { command: Some(command.into()), + near_current_pane, }), crate::input::actions::Action::Detach => ActionType::Detach(DetachAction {}), crate::input::actions::Action::SwitchSession { @@ -1112,16 +1144,20 @@ impl From<crate::input::actions::Action> crate::input::actions::Action::FocusTerminalPaneWithId { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => ActionType::FocusTerminalPaneWithId(FocusTerminalPaneWithIdAction { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }), crate::input::actions::Action::FocusPluginPaneWithId { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => ActionType::FocusPluginPaneWithId(FocusPluginPaneWithIdAction { pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }), crate::input::actions::Action::RenameTerminalPane { pane_id, name } => { ActionType::RenameTerminalPane(RenameTerminalPaneAction { @@ -1380,6 +1416,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> .coordinates .map(|c| c.try_into()) .transpose()?, + near_current_pane: edit_file_action.near_current_pane, }), ActionType::NewFloatingPane(new_floating_action) => { Ok(crate::input::actions::Action::NewFloatingPane { @@ -1392,6 +1429,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> .coordinates .map(|c| c.try_into()) .transpose()?, + near_current_pane: new_floating_action.near_current_pane, }) }, ActionType::NewTiledPane(new_tiled_action) => { @@ -1402,6 +1440,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> .transpose()?, command: new_tiled_action.command.map(|c| c.try_into()).transpose()?, pane_name: new_tiled_action.pane_name, + near_current_pane: new_tiled_action.near_current_pane, }) }, ActionType::NewInPlacePane(new_in_place_action) => { @@ -1411,6 +1450,11 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> .map(|c| c.try_into()) .transpose()?, pane_name: new_in_place_action.pane_name, + near_current_pane: new_in_place_action.near_current_pane, + pane_id_to_replace: new_in_place_action + .pane_id_to_replace + .and_then(|p| p.try_into().ok()), + close_replace_pane: new_in_place_action.close_replace_pane, }) }, ActionType::NewStackedPane(new_stacked_action) => { @@ -1420,6 +1464,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> .map(|c| c.try_into()) .transpose()?, pane_name: new_stacked_action.pane_name, + near_current_pane: new_stacked_action.near_current_pane, }) }, ActionType::NewBlockingPane(new_blocking_action) => { @@ -1433,6 +1478,11 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> .command .map(|c| c.try_into()) .transpose()?, + unblock_condition: new_blocking_action + .unblock_condition + .map(|c| proto_i32_to_unblock_condition(c)) + .transpose()?, + near_current_pane: new_blocking_action.near_current_pane, }) }, ActionType::TogglePaneEmbedOrFloating(_) => { @@ -1487,6 +1537,22 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> tab_name: new_tab_action.tab_name, should_change_focus_to_new_tab: new_tab_action.should_change_focus_to_new_tab, cwd: new_tab_action.cwd.map(PathBuf::from), + initial_panes: if new_tab_action.initial_panes.is_empty() { + None + } else { + Some( + new_tab_action + .initial_panes + .into_iter() + .map(|p| p.try_into()) + .collect::<Result<Vec<_>>>()?, + ) + }, + + first_pane_unblock_condition: new_tab_action + .first_pane_unblock_condition + .map(|c| proto_i32_to_unblock_condition(c)) + .transpose()?, }), ActionType::NoOp(_) => Ok(crate::input::actions::Action::NoOp), ActionType::GoToNextTab(_) => Ok(crate::input::actions::Action::GoToNextTab), @@ -1516,6 +1582,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> .command .ok_or_else(|| anyhow!("Run missing command"))? .try_into()?, + near_current_pane: run_action.near_current_pane, }), ActionType::Detach(_) => Ok(crate::input::actions::Action::Detach), ActionType::SwitchSession(switch_session_action) => { @@ -1661,12 +1728,14 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> Ok(crate::input::actions::Action::FocusTerminalPaneWithId { pane_id: focus_pane_action.pane_id, should_float_if_hidden: focus_pane_action.should_float_if_hidden, + should_be_in_place_if_hidden: focus_pane_action.should_be_in_place_if_hidden, }) }, ActionType::FocusPluginPaneWithId(focus_pane_action) => { Ok(crate::input::actions::Action::FocusPluginPaneWithId { pane_id: focus_pane_action.pane_id, should_float_if_hidden: focus_pane_action.should_float_if_hidden, + should_be_in_place_if_hidden: focus_pane_action.should_be_in_place_if_hidden, }) }, ActionType::RenameTerminalPane(rename_pane_action) => { @@ -2037,6 +2106,15 @@ fn search_option_to_proto_i32(option: crate::input::actions::SearchOption) -> i3 } } +fn unblock_condition_to_proto_i32(condition: crate::data::UnblockCondition) -> i32 { + use crate::client_server_contract::client_server_contract::UnblockCondition as ProtoUnblockCondition; + match condition { + crate::data::UnblockCondition::OnExitSuccess => ProtoUnblockCondition::OnExitSuccess as i32, + crate::data::UnblockCondition::OnExitFailure => ProtoUnblockCondition::OnExitFailure as i32, + crate::data::UnblockCondition::OnAnyExit => ProtoUnblockCondition::OnAnyExit as i32, + } +} + // Reverse helper functions for Action conversion fn proto_i32_to_resize(resize: i32) -> Result<crate::data::Resize> { @@ -2103,6 +2181,26 @@ fn proto_i32_to_search_option(option: i32) -> Result<crate::input::actions::Sear } } +fn proto_i32_to_unblock_condition(condition: i32) -> Result<crate::data::UnblockCondition> { + use crate::client_server_contract::client_server_contract::UnblockCondition as ProtoUnblockCondition; + let proto_condition = match condition { + x if x == ProtoUnblockCondition::OnExitSuccess as i32 => { + ProtoUnblockCondition::OnExitSuccess + }, + x if x == ProtoUnblockCondition::OnExitFailure as i32 => { + ProtoUnblockCondition::OnExitFailure + }, + x if x == ProtoUnblockCondition::OnAnyExit as i32 => ProtoUnblockCondition::OnAnyExit, + _ => return Err(anyhow!("Invalid UnblockCondition: {}", condition)), + }; + match proto_condition { + ProtoUnblockCondition::OnExitSuccess => Ok(crate::data::UnblockCondition::OnExitSuccess), + ProtoUnblockCondition::OnExitFailure => Ok(crate::data::UnblockCondition::OnExitFailure), + ProtoUnblockCondition::OnAnyExit => Ok(crate::data::UnblockCondition::OnAnyExit), + ProtoUnblockCondition::Unspecified => Err(anyhow!("Unspecified unblock condition")), + } +} + // Position conversion impl From<crate::position::Position> for crate::client_server_contract::client_server_contract::Position @@ -2722,6 +2820,47 @@ impl From<crate::input::layout::RunPluginOrAlias> } } +// CommandOrPlugin conversion +impl From<crate::data::CommandOrPlugin> + for crate::client_server_contract::client_server_contract::CommandOrPlugin +{ + fn from(cmd_or_plugin: crate::data::CommandOrPlugin) -> Self { + use crate::client_server_contract::client_server_contract::command_or_plugin::CommandOrPluginType; + match cmd_or_plugin { + crate::data::CommandOrPlugin::Command(cmd) => Self { + command_or_plugin_type: Some(CommandOrPluginType::Command(cmd.into())), + }, + crate::data::CommandOrPlugin::Plugin(plugin) => Self { + command_or_plugin_type: Some(CommandOrPluginType::Plugin(plugin.into())), + }, + } + } +} + +impl TryFrom<crate::client_server_contract::client_server_contract::CommandOrPlugin> + for crate::data::CommandOrPlugin +{ + type Error = anyhow::Error; + + fn try_from( + proto: crate::client_server_contract::client_server_contract::CommandOrPlugin, + ) -> Result<Self> { + use crate::client_server_contract::client_server_contract::command_or_plugin::CommandOrPluginType; + + let cmd_or_plugin_type = proto + .command_or_plugin_type + .ok_or_else(|| anyhow!("CommandOrPlugin missing command_or_plugin_type"))?; + match cmd_or_plugin_type { + CommandOrPluginType::Command(cmd) => { + Ok(crate::data::CommandOrPlugin::Command(cmd.try_into()?)) + }, + CommandOrPluginType::Plugin(plugin) => { + Ok(crate::data::CommandOrPlugin::Plugin(plugin.try_into()?)) + }, + } + } +} + // Run reverse conversion impl TryFrom<crate::client_server_contract::client_server_contract::Run> for crate::input::layout::Run diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs index 2ec513da..61981f38 100644 --- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs +++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs @@ -1,7 +1,8 @@ use super::test_framework::*; use crate::data::{ - BareKey, ConnectToSession, Direction, FloatingPaneCoordinates, InputMode, KeyModifier, - KeyWithModifier, LayoutInfo, OriginatingPlugin, PaneId, PluginTag, Resize, WebSharing, + BareKey, CommandOrPlugin, ConnectToSession, Direction, FloatingPaneCoordinates, InputMode, + KeyModifier, KeyWithModifier, LayoutInfo, OriginatingPlugin, PaneId, PluginTag, Resize, + WebSharing, }; use crate::input::actions::{Action, SearchDirection, SearchOption}; use crate::input::cli_assets::CliAssets; @@ -1019,6 +1020,7 @@ fn test_client_messages() { in_place: false, start_suppressed: false, coordinates: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1041,6 +1043,7 @@ fn test_client_messages() { in_place: false, start_suppressed: false, coordinates: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1063,6 +1066,7 @@ fn test_client_messages() { in_place: false, start_suppressed: false, coordinates: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1085,6 +1089,7 @@ fn test_client_messages() { in_place: true, start_suppressed: true, coordinates: FloatingPaneCoordinates::new(None, None, None, None, None), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1113,6 +1118,7 @@ fn test_client_messages() { None, None ), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1141,6 +1147,7 @@ fn test_client_messages() { None, None ), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1169,6 +1176,7 @@ fn test_client_messages() { Some("20".to_owned()), Some(true) ), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1191,6 +1199,7 @@ fn test_client_messages() { in_place: true, start_suppressed: true, coordinates: FloatingPaneCoordinates::new(None, None, None, None, Some(false)), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1201,6 +1210,7 @@ fn test_client_messages() { command: None, pane_name: None, coordinates: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1225,7 +1235,8 @@ fn test_client_messages() { None, None, None - ) + ), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1254,7 +1265,8 @@ fn test_client_messages() { None, None, None - ) + ), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1265,6 +1277,7 @@ fn test_client_messages() { command: None, direction: None, pane_name: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1288,6 +1301,7 @@ fn test_client_messages() { }), direction: Some(Direction::Right), pane_name: Some("my_pane_name".to_owned()), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1310,6 +1324,9 @@ fn test_client_messages() { use_terminal_title: false, }), pane_name: Some("my_pane_name".to_owned()), + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1319,6 +1336,9 @@ fn test_client_messages() { action: Action::NewInPlacePane { command: None, pane_name: None, + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1341,6 +1361,7 @@ fn test_client_messages() { use_terminal_title: false, }), pane_name: Some("my_pane_name".to_owned()), + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1350,6 +1371,7 @@ fn test_client_messages() { action: Action::NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -1396,6 +1418,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1410,6 +1434,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1427,6 +1453,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1444,6 +1472,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1462,6 +1492,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1492,6 +1524,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1509,6 +1543,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1526,6 +1562,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1545,6 +1583,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1565,6 +1605,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1585,6 +1627,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1605,6 +1649,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1625,6 +1671,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1645,6 +1693,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1665,6 +1715,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1685,6 +1737,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1705,6 +1759,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1722,6 +1778,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1745,6 +1803,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1770,6 +1830,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1799,6 +1861,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1816,6 +1880,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1837,6 +1903,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1854,6 +1922,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1891,6 +1961,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1913,6 +1985,8 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1927,6 +2001,8 @@ fn test_client_messages() { tab_name: Some("tab_name".to_owned()), should_change_focus_to_new_tab: false, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -1941,6 +2017,36 @@ fn test_client_messages() { tab_name: None, should_change_focus_to_new_tab: true, cwd: Some(PathBuf::from("relative/path/to/cwd")), + initial_panes: None, + first_pane_unblock_condition: None, + }, + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::NewTab { + tiled_layout: None, + floating_layouts: vec![], + swap_tiled_layouts: None, + swap_floating_layouts: None, + tab_name: None, + should_change_focus_to_new_tab: true, + cwd: None, + initial_panes: Some(vec![ + CommandOrPlugin::Command(RunCommandAction { + command: PathBuf::from("/path/to/command"), + args: vec![], + cwd: None, + direction: None, + hold_on_close: false, + hold_on_start: false, + originating_plugin: None, + use_terminal_title: false, + }), + CommandOrPlugin::Plugin(RunPluginOrAlias::RunPlugin(RunPlugin::default())), + ]), + first_pane_unblock_condition: None, }, terminal_id: Some(1), client_id: Some(100), @@ -2035,6 +2141,7 @@ fn test_client_messages() { originating_plugin: None, use_terminal_title: false, }, + near_current_pane: false, }, terminal_id: Some(1), client_id: Some(100), @@ -2328,6 +2435,7 @@ fn test_client_messages() { action: Action::FocusTerminalPaneWithId { pane_id: 12, should_float_if_hidden: false, + should_be_in_place_if_hidden: false, }, terminal_id: Some(1), client_id: Some(100), @@ -2337,6 +2445,7 @@ fn test_client_messages() { action: Action::FocusTerminalPaneWithId { pane_id: 12, should_float_if_hidden: true, + should_be_in_place_if_hidden: false, }, terminal_id: Some(1), client_id: Some(100), @@ -2346,6 +2455,7 @@ fn test_client_messages() { action: Action::FocusPluginPaneWithId { pane_id: 12, should_float_if_hidden: false, + should_be_in_place_if_hidden: true, }, terminal_id: Some(1), client_id: Some(100), @@ -2355,6 +2465,7 @@ fn test_client_messages() { action: Action::FocusPluginPaneWithId { pane_id: 12, should_float_if_hidden: true, + should_be_in_place_if_hidden: false, }, terminal_id: Some(1), client_id: Some(100), diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 789c3d96..5ca02d86 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -1,8 +1,8 @@ mod kdl_layout_parser; use crate::data::{ BareKey, Direction, FloatingPaneCoordinates, InputMode, KeyWithModifier, LayoutInfo, - MultiplayerColors, Palette, PaletteColor, PaneInfo, PaneManifest, PermissionType, Resize, - SessionInfo, StyleDeclaration, Styling, TabInfo, WebSharing, DEFAULT_STYLES, + MultiplayerColors, Palette, PaletteColor, PaneId, PaneInfo, PaneManifest, PermissionType, + Resize, SessionInfo, StyleDeclaration, Styling, TabInfo, WebSharing, DEFAULT_STYLES, }; use crate::envs::EnvironmentVariables; use crate::home::{find_default_config_dir, get_layout_dir}; @@ -551,6 +551,7 @@ impl Action { return Ok(Action::NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }); } else { let direction = Direction::from_str(string.as_str()).map_err(|_| { @@ -742,6 +743,8 @@ impl Action { tab_name: name, should_change_focus_to_new_tab, cwd, + initial_panes: _, + first_pane_unblock_condition: _, } => { let mut node = KdlNode::new("NewTab"); let mut children = KdlDocument::new(); @@ -800,6 +803,7 @@ impl Action { direction, command: run_command_action, pane_name: name, + near_current_pane: false, } => { let mut node = KdlNode::new("Run"); let mut node_children = KdlDocument::new(); @@ -849,6 +853,7 @@ impl Action { command: run_command_action, pane_name: name, coordinates: floating_pane_coordinates, + near_current_pane: false, } => { let mut node = KdlNode::new("Run"); let mut node_children = KdlDocument::new(); @@ -939,6 +944,9 @@ impl Action { Action::NewInPlacePane { command: run_command_action, pane_name: name, + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, } => { let mut node = KdlNode::new("Run"); let mut node_children = KdlDocument::new(); @@ -979,6 +987,7 @@ impl Action { Action::NewStackedPane { command: run_command_action, pane_name: name, + near_current_pane: _, } => match run_command_action { Some(run_command_action) => { let mut node = KdlNode::new("Run"); @@ -1623,6 +1632,8 @@ impl TryFrom<(&KdlNode, &Options)> for Action { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }); } @@ -1692,6 +1703,8 @@ impl TryFrom<(&KdlNode, &Options)> for Action { tab_name: name, should_change_focus_to_new_tab, cwd, + initial_panes: None, + first_pane_unblock_condition: None, }) } else { let (layout, floating_panes_layout) = layout.new_tab(); @@ -1705,6 +1718,8 @@ impl TryFrom<(&KdlNode, &Options)> for Action { tab_name: name, should_change_focus_to_new_tab, cwd, + initial_panes: None, + first_pane_unblock_condition: None, }) } }, @@ -1785,22 +1800,28 @@ impl TryFrom<(&KdlNode, &Options)> for Action { command: Some(run_command_action), pane_name: name, coordinates: FloatingPaneCoordinates::new(x, y, width, height, pinned), + near_current_pane: false, }) } else if in_place { Ok(Action::NewInPlacePane { command: Some(run_command_action), pane_name: name, + near_current_pane: false, + pane_id_to_replace: None, + close_replace_pane: false, }) } else if stacked { Ok(Action::NewStackedPane { command: Some(run_command_action), pane_name: name, + near_current_pane: false, }) } else { Ok(Action::NewTiledPane { direction, command: Some(run_command_action), pane_name: name, + near_current_pane: false, }) } }, @@ -5073,6 +5094,50 @@ impl SessionInfo { } } } + let mut pane_history = BTreeMap::new(); + if let Some(kdl_pane_history) = kdl_document.get("pane_history").and_then(|p| p.children()) + { + for client_node in kdl_pane_history.nodes() { + if let Some(client_id) = client_node.children().and_then(|c| { + c.get("id") + .and_then(|c| c.entries().iter().next().and_then(|e| e.value().as_i64())) + }) { + let mut history = vec![]; + if let Some(history_node) = + client_node.children().and_then(|c| c.get("history")) + { + if let Some(history_children) = history_node.children() { + for pane_id_node in history_children.nodes() { + if pane_id_node.name().value() == "pane_id" { + let pane_type = pane_id_node + .entries() + .iter() + .find(|e| e.name().map(|n| n.value()) == Some("type")) + .and_then(|e| e.value().as_string()); + let id = pane_id_node + .entries() + .iter() + .find(|e| e.name().is_none()) + .and_then(|e| e.value().as_i64()) + .map(|i| i as u32); + if let (Some(pane_type), Some(id)) = (pane_type, id) { + let pane_id = match pane_type { + "terminal" => Some(PaneId::Terminal(id)), + "plugin" => Some(PaneId::Plugin(id)), + _ => None, + }; + if let Some(pane_id) = pane_id { + history.push(pane_id); + } + } + } + } + } + } + pane_history.insert(client_id as u16, history); + } + } + } Ok(SessionInfo { name, tabs, @@ -5084,6 +5149,7 @@ impl SessionInfo { web_clients_allowed, plugins: Default::default(), // we do not serialize plugin information tab_history, + pane_history, }) } pub fn to_string(&self) -> String { @@ -5148,6 +5214,35 @@ impl SessionInfo { } tab_history.set_children(tab_history_children); + let mut pane_history = KdlNode::new("pane_history"); + let mut pane_history_children = KdlDocument::new(); + for (client_id, client_pane_history) in &self.pane_history { + let mut client_document = KdlDocument::new(); + let mut client_node = KdlNode::new("client"); + let mut id = KdlNode::new("id"); + id.push(*client_id as i64); + client_document.nodes_mut().push(id); + let mut history = KdlNode::new("history"); + for pane_id in client_pane_history { + let mut pane_id_node = KdlNode::new("pane_id"); + match pane_id { + PaneId::Terminal(id) => { + pane_id_node.push(KdlEntry::new_prop("type", "terminal")); + pane_id_node.push(*id as i64); + }, + PaneId::Plugin(id) => { + pane_id_node.push(KdlEntry::new_prop("type", "plugin")); + pane_id_node.push(*id as i64); + }, + } + history.ensure_children().nodes_mut().push(pane_id_node); + } + client_document.nodes_mut().push(history); + client_node.set_children(client_document); + pane_history_children.nodes_mut().push(client_node); + } + pane_history.set_children(pane_history_children); + kdl_document.nodes_mut().push(name); kdl_document.nodes_mut().push(tabs); kdl_document.nodes_mut().push(panes); @@ -5156,6 +5251,7 @@ impl SessionInfo { kdl_document.nodes_mut().push(web_client_count); kdl_document.nodes_mut().push(available_layouts); kdl_document.nodes_mut().push(tab_history); + kdl_document.nodes_mut().push(pane_history); kdl_document.fmt(); kdl_document.to_string() } @@ -5714,6 +5810,7 @@ fn serialize_and_deserialize_session_info_with_data() { web_client_count: 2, web_clients_allowed: true, tab_history: Default::default(), + pane_history: Default::default(), }; let serialized = session_info.to_string(); let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap(); 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 11f6e53d..6353ee0e 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 @@ -122,6 +122,12 @@ keybinds clear-defaults=true { } SwitchToMode "normal" } + bind "q" { + LaunchPlugin "zellij:sequence" { + floating true + } + SwitchToMode "normal" + } bind "s" { LaunchOrFocusPlugin "zellij:share" { floating true @@ -255,6 +261,7 @@ plugins { cwd "/" } plugin-manager location="zellij:plugin-manager" + sequence location="zellij:sequence" session-manager location="zellij:session-manager" status-bar location="zellij:status-bar" strider location="zellij:strider" 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 ddf36a40..e562e030 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 @@ -122,6 +122,12 @@ keybinds clear-defaults=true { } SwitchToMode "normal" } + bind "q" { + LaunchPlugin "zellij:sequence" { + floating true + } + SwitchToMode "normal" + } bind "s" { LaunchOrFocusPlugin "zellij:share" { floating true @@ -258,6 +264,7 @@ plugins { cwd "/" } plugin-manager location="zellij:plugin-manager" + sequence location="zellij:sequence" session-manager location="zellij:session-manager" status-bar location="zellij:status-bar" strider location="zellij:strider" diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap index 088504af..e43fcf3b 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap @@ -14,3 +14,5 @@ available_layouts { } tab_history { } +pane_history { +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap index cd4ff687..44b2e8bf 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap @@ -98,3 +98,5 @@ available_layouts { } tab_history { } +pane_history { +} diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index 2772e725..c29884af 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -5,6 +5,163 @@ import "resize.proto"; package api.action; +// Layout and related types + +enum SplitDirection { + SPLIT_DIRECTION_UNSPECIFIED = 0; + SPLIT_DIRECTION_HORIZONTAL = 1; + SPLIT_DIRECTION_VERTICAL = 2; +} + +message PercentOrFixed { + oneof size_type { + uint32 percent = 1; + uint32 fixed = 2; + } +} + +enum LayoutConstraint { + LAYOUT_CONSTRAINT_UNSPECIFIED = 0; + LAYOUT_CONSTRAINT_MAX_PANES = 1; + LAYOUT_CONSTRAINT_MIN_PANES = 2; + LAYOUT_CONSTRAINT_EXACT_PANES = 3; + LAYOUT_CONSTRAINT_NO_CONSTRAINT = 4; +} + +message LayoutConstraintWithValue { + LayoutConstraint constraint_type = 1; + optional uint32 value = 2; +} + +message PluginUserConfiguration { + map<string, string> configuration = 1; +} + +enum RunPluginLocation { + RUN_PLUGIN_LOCATION_UNSPECIFIED = 0; + RUN_PLUGIN_LOCATION_FILE = 1; + RUN_PLUGIN_LOCATION_ZELLIJ = 2; + RUN_PLUGIN_LOCATION_REMOTE = 3; +} + +message RunPluginLocationData { + RunPluginLocation location_type = 1; + oneof location_data { + string file_path = 2; + PluginTag zellij_tag = 3; + string remote_url = 4; + } +} + +message PluginTag { + string tag = 1; +} + +message RunPlugin { + bool _allow_exec_host_cmd = 1; + RunPluginLocationData location = 2; + PluginUserConfiguration configuration = 3; + optional string initial_cwd = 4; +} + +message PluginAlias { + string name = 1; + optional PluginUserConfiguration configuration = 2; + optional string initial_cwd = 3; + optional RunPlugin run_plugin = 4; +} + +message RunPluginOrAlias { + oneof plugin_type { + RunPlugin plugin = 1; + PluginAlias alias = 2; + } +} + +message RunEditFileAction { + string file_path = 1; + optional uint32 line_number = 2; + optional string cwd = 3; +} + +message PaneRun { + oneof run_type { + RunCommandAction command = 1; + RunPluginOrAlias plugin = 2; + RunEditFileAction edit_file = 3; + string cwd = 4; + } +} + +message TiledPaneLayout { + SplitDirection children_split_direction = 1; + optional string name = 2; + repeated TiledPaneLayout children = 3; + optional SplitSize split_size = 4; + optional PaneRun run = 5; + bool borderless = 6; + optional string focus = 7; + optional bool exclude_from_sync = 8; + bool children_are_stacked = 9; + optional uint32 external_children_index = 10; + bool is_expanded_in_stack = 11; + bool hide_floating_panes = 12; + optional string pane_initial_contents = 13; +} + +message FloatingPaneLayout { + optional string name = 1; + optional PercentOrFixed height = 2; + optional PercentOrFixed width = 3; + optional PercentOrFixed x = 4; + optional PercentOrFixed y = 5; + optional bool pinned = 6; + optional PaneRun run = 7; + optional bool focus = 8; + bool already_running = 9; + optional string pane_initial_contents = 10; + optional uint32 logical_position = 11; +} + +message SwapTiledLayout { + repeated LayoutConstraintTiledPair constraint_map = 1; + optional string name = 2; +} + +message SwapFloatingLayout { + repeated LayoutConstraintFloatingPair constraint_map = 1; + optional string name = 2; +} + +message LayoutConstraintTiledPair { + LayoutConstraintWithValue constraint = 1; + TiledPaneLayout layout = 2; +} + +message LayoutConstraintFloatingPair { + LayoutConstraintWithValue constraint = 1; + repeated FloatingPaneLayout layouts = 2; +} + +message CommandOrPlugin { + oneof command_or_plugin_type { + RunCommandAction command = 1; + RunPluginOrAlias plugin = 2; + } +} + +message NewTabPayload { + optional TiledPaneLayout tiled_layout = 1; + repeated FloatingPaneLayout floating_layouts = 2; + repeated SwapTiledLayout swap_tiled_layouts = 3; + repeated SwapFloatingLayout swap_floating_layouts = 4; + optional string tab_name = 5; + bool should_change_focus_to_new_tab = 6; + optional string cwd = 7; + repeated CommandOrPlugin initial_panes = 8; + optional UnblockCondition first_pane_unblock_condition = 9; +} + message Action { ActionName name = 1; oneof optional_payload { @@ -53,6 +210,9 @@ message Action { CliPipePayload message_payload = 47; MoveTabDirection move_tab_payload = 48; MouseEventPayload mouse_event_payload = 49; + NewBlockingPanePayload new_blocking_pane_payload = 50; + NewTabPayload new_tab_payload = 51; + NewInPlacePanePayload new_in_place_pane_payload = 52; } } @@ -71,6 +231,7 @@ message IdAndName { message PaneIdAndShouldFloat { uint32 pane_id = 1; bool should_float = 2; + bool should_be_in_place = 3; } message NewPluginPanePayload { @@ -111,11 +272,13 @@ message GoToTabNamePayload { message NewFloatingPanePayload { optional RunCommandAction command = 1; + bool near_current_pane = 2; } message NewTiledPanePayload { optional RunCommandAction command = 1; optional resize.ResizeDirection direction = 2; + bool near_current_pane = 3; } message MovePanePayload { @@ -128,6 +291,7 @@ message EditFilePayload { optional string cwd = 3; optional resize.ResizeDirection direction = 4; bool should_float = 5; + bool near_current_pane = 6; } message ScrollAtPayload { @@ -139,12 +303,30 @@ message NewPanePayload { optional string pane_name = 2; } +message NewBlockingPanePayload { + NewPanePlacement placement = 1; + optional string pane_name = 2; + optional RunCommandAction command = 3; + optional UnblockCondition unblock_condition = 4; + bool near_current_pane = 5; +} + +message NewInPlacePanePayload { + optional RunCommandAction command = 1; + optional string pane_name = 2; + bool near_current_pane = 3; + optional PaneId pane_id_to_replace = 4; + bool close_replace_pane = 5; +} + message SwitchToModePayload { input_mode.InputMode input_mode = 1; } message WritePayload { bytes bytes_to_write = 1; + optional KeyWithModifier key_with_modifier = 2; + bool is_kitty_keyboard_protocol = 3; } message WriteCharsPayload { @@ -245,6 +427,8 @@ enum ActionName { ToggleGroupMarking = 88; NewStackedPane = 89; SwitchSession = 90; + NewBlockingPane = 91; + NewInPlacePane = 92; } message Position { @@ -252,6 +436,66 @@ message Position { int64 column = 2; } +// SplitSize represents a dimension that can be either a percentage or fixed size +message SplitSize { + oneof split_size_variant { + uint32 percent = 1; // 1 to 100 + uint32 fixed = 2; // absolute number of columns or rows + } +} + +// PaneId identifies either a terminal or plugin pane +message PaneId { + oneof pane_id_variant { + uint32 terminal = 1; + uint32 plugin = 2; + } +} + +// FloatingPaneCoordinates specifies the position and size of a floating pane +message FloatingPaneCoordinates { + optional SplitSize x = 1; + optional SplitSize y = 2; + optional SplitSize width = 3; + optional SplitSize height = 4; + optional bool pinned = 5; +} + +// UnblockCondition specifies when a blocking pane should unblock +enum UnblockCondition { + UNBLOCK_ON_EXIT_SUCCESS = 0; + UNBLOCK_ON_EXIT_FAILURE = 1; + UNBLOCK_ON_ANY_EXIT = 2; +} + +// NewPanePlacement specifies where a new pane should be placed +message NewPanePlacement { + oneof placement_variant { + bool no_preference = 1; + TiledPlacement tiled = 2; + FloatingPlacement floating = 3; + InPlaceConfig in_place = 4; + StackedPlacement stacked = 5; + } +} + +message TiledPlacement { + optional resize.ResizeDirection direction = 1; +} + +message FloatingPlacement { + optional FloatingPaneCoordinates coordinates = 1; +} + +message InPlaceConfig { + optional PaneId pane_id_to_replace = 1; + bool close_replaced_pane = 2; +} + +message StackedPlacement { + optional PaneId pane_id = 1; +} + message MouseEventPayload { uint32 event_type = 1; bool left = 2; @@ -284,3 +528,54 @@ message NameAndValue { string name = 1; string value = 2; } + +message KeyWithModifier { + BareKey bare_key = 1; + repeated KeyModifier key_modifiers = 2; + optional string character = 3; // Only set when bare_key is CHAR +} + +enum BareKey { + BARE_KEY_UNSPECIFIED = 0; + BARE_KEY_PAGE_DOWN = 1; + BARE_KEY_PAGE_UP = 2; + BARE_KEY_LEFT = 3; + BARE_KEY_DOWN = 4; + BARE_KEY_UP = 5; + BARE_KEY_RIGHT = 6; + BARE_KEY_HOME = 7; + BARE_KEY_END = 8; + BARE_KEY_BACKSPACE = 9; + BARE_KEY_DELETE = 10; + BARE_KEY_INSERT = 11; + BARE_KEY_F1 = 12; + BARE_KEY_F2 = 13; + BARE_KEY_F3 = 14; + BARE_KEY_F4 = 15; + BARE_KEY_F5 = 16; + BARE_KEY_F6 = 17; + BARE_KEY_F7 = 18; + BARE_KEY_F8 = 19; + BARE_KEY_F9 = 20; + BARE_KEY_F10 = 21; + BARE_KEY_F11 = 22; + BARE_KEY_F12 = 23; + BARE_KEY_CHAR = 24; + BARE_KEY_TAB = 25; + BARE_KEY_ESC = 26; + BARE_KEY_ENTER = 27; + BARE_KEY_CAPS_LOCK = 28; + BARE_KEY_SCROLL_LOCK = 29; + BARE_KEY_NUM_LOCK = 30; + BARE_KEY_PRINT_SCREEN = 31; + BARE_KEY_PAUSE = 32; + BARE_KEY_MENU = 33; +} + +enum KeyModifier { + KEY_MODIFIER_UNSPECIFIED = 0; + KEY_MODIFIER_CTRL = 1; + KEY_MODIFIER_ALT = 2; + KEY_MODIFIER_SHIFT = 3; + KEY_MODIFIER_SUPER = 4; +} diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index c442144a..f8de7b76 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -1,26 +1,88 @@ pub use super::generated_api::api::{ action::{ - action::OptionalPayload, Action as ProtobufAction, ActionName as ProtobufActionName, - DumpScreenPayload, EditFilePayload, GoToTabNamePayload, IdAndName, - LaunchOrFocusPluginPayload, MouseEventPayload as ProtobufMouseEventPayload, - MovePanePayload, MoveTabDirection as ProtobufMoveTabDirection, - NameAndValue as ProtobufNameAndValue, NewFloatingPanePayload, NewPanePayload, - NewPluginPanePayload, NewTiledPanePayload, PaneIdAndShouldFloat, - PluginConfiguration as ProtobufPluginConfiguration, Position as ProtobufPosition, - RunCommandAction as ProtobufRunCommandAction, ScrollAtPayload, - SearchDirection as ProtobufSearchDirection, SearchOption as ProtobufSearchOption, - SwitchToModePayload, WriteCharsPayload, WritePayload, + action::OptionalPayload, + command_or_plugin::CommandOrPluginType, + pane_run::RunType, + run_plugin_location_data::LocationData, + run_plugin_or_alias::PluginType, + Action as ProtobufAction, + ActionName as ProtobufActionName, + BareKey as ProtobufBareKey, + // New layout-related types + CommandOrPlugin as ProtobufCommandOrPlugin, + DumpScreenPayload, + EditFilePayload, + FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, + FloatingPaneLayout as ProtobufFloatingPaneLayout, + FloatingPlacement as ProtobufFloatingPlacement, + GoToTabNamePayload, + IdAndName, + InPlaceConfig as ProtobufInPlaceConfig, + KeyModifier as ProtobufKeyModifier, + KeyWithModifier as ProtobufKeyWithModifier, + LaunchOrFocusPluginPayload, + LayoutConstraint as ProtobufLayoutConstraint, + LayoutConstraintFloatingPair as ProtobufLayoutConstraintFloatingPair, + LayoutConstraintTiledPair as ProtobufLayoutConstraintTiledPair, + LayoutConstraintWithValue as ProtobufLayoutConstraintWithValue, + MouseEventPayload as ProtobufMouseEventPayload, + MovePanePayload, + MoveTabDirection as ProtobufMoveTabDirection, + NameAndValue as ProtobufNameAndValue, + NewBlockingPanePayload, + NewFloatingPanePayload, + NewInPlacePanePayload, + NewPanePayload, + NewPanePlacement as ProtobufNewPanePlacement, + NewPluginPanePayload, + NewTabPayload, + NewTiledPanePayload, + PaneId as ProtobufPaneId, + PaneIdAndShouldFloat, + PaneRun as ProtobufPaneRun, + PercentOrFixed as ProtobufPercentOrFixed, + PluginAlias as ProtobufPluginAlias, + PluginConfiguration as ProtobufPluginConfiguration, + PluginTag as ProtobufPluginTag, + PluginUserConfiguration as ProtobufPluginUserConfiguration, + Position as ProtobufPosition, + RunCommandAction as ProtobufRunCommandAction, + RunEditFileAction as ProtobufRunEditFileAction, + RunPlugin as ProtobufRunPlugin, + RunPluginLocation as ProtobufRunPluginLocation, + RunPluginLocationData as ProtobufRunPluginLocationData, + RunPluginOrAlias as ProtobufRunPluginOrAlias, + ScrollAtPayload, + SearchDirection as ProtobufSearchDirection, + SearchOption as ProtobufSearchOption, + SplitDirection as ProtobufSplitDirection, + SplitSize as ProtobufSplitSize, + StackedPlacement as ProtobufStackedPlacement, + SwapFloatingLayout as ProtobufSwapFloatingLayout, + SwapTiledLayout as ProtobufSwapTiledLayout, + SwitchToModePayload, + TiledPaneLayout as ProtobufTiledPaneLayout, + TiledPlacement as ProtobufTiledPlacement, + UnblockCondition as ProtobufUnblockCondition, + WriteCharsPayload, + WritePayload, }, input_mode::InputMode as ProtobufInputMode, resize::{Resize as ProtobufResize, ResizeDirection as ProtobufResizeDirection}, }; -use crate::data::{Direction, InputMode, ResizeStrategy}; +use crate::data::{ + CommandOrPlugin, Direction, FloatingPaneCoordinates, InputMode, KeyWithModifier, + NewPanePlacement, PaneId, PluginTag, ResizeStrategy, UnblockCondition, +}; use crate::errors::prelude::*; use crate::input::actions::Action; use crate::input::actions::{SearchDirection, SearchOption}; use crate::input::command::{OpenFilePayload, RunCommandAction}; +use crate::input::layout::SplitSize; use crate::input::layout::{ - PluginUserConfiguration, RunPlugin, RunPluginLocation, RunPluginOrAlias, + FloatingPaneLayout, LayoutConstraint, PercentOrFixed, PluginAlias, PluginUserConfiguration, + Run, RunPlugin, RunPluginLocation, RunPluginOrAlias, SplitDirection, SwapFloatingLayout, + SwapTiledLayout, TiledPaneLayout, }; use crate::input::mouse::{MouseEvent, MouseEventType}; use crate::position::Position; @@ -38,11 +100,16 @@ impl TryFrom<ProtobufAction> for Action { None => Ok(Action::Quit), }, Some(ProtobufActionName::Write) => match protobuf_action.optional_payload { - Some(OptionalPayload::WritePayload(write_payload)) => Ok(Action::Write { - key_with_modifier: None, - bytes: write_payload.bytes_to_write, - is_kitty_keyboard_protocol: false, - }), + Some(OptionalPayload::WritePayload(write_payload)) => { + let key_with_modifier = write_payload + .key_with_modifier + .and_then(|k| k.try_into().ok()); + Ok(Action::Write { + key_with_modifier, + bytes: write_payload.bytes_to_write, + is_kitty_keyboard_protocol: write_payload.is_kitty_keyboard_protocol, + }) + }, _ => Err("Wrong payload for Action::Write"), }, Some(ProtobufActionName::WriteChars) => match protobuf_action.optional_payload { @@ -246,6 +313,7 @@ impl TryFrom<ProtobufAction> for Action { .direction .and_then(|d| ProtobufResizeDirection::from_i32(d)) .and_then(|d| d.try_into().ok()); + let near_current_pane = payload.near_current_pane; let should_float = payload.should_float; let should_be_in_place = false; Ok(Action::EditFile { @@ -255,12 +323,14 @@ impl TryFrom<ProtobufAction> for Action { in_place: should_be_in_place, start_suppressed: false, coordinates: None, + near_current_pane, }) }, _ => Err("Wrong payload for Action::NewPane"), }, Some(ProtobufActionName::NewFloatingPane) => match protobuf_action.optional_payload { Some(OptionalPayload::NewFloatingPanePayload(payload)) => { + let near_current_pane = payload.near_current_pane; if let Some(payload) = payload.command { let pane_name = payload.pane_name.clone(); let run_command_action: RunCommandAction = payload.try_into()?; @@ -268,12 +338,14 @@ impl TryFrom<ProtobufAction> for Action { command: Some(run_command_action), pane_name, coordinates: None, + near_current_pane, }) } else { Ok(Action::NewFloatingPane { command: None, pane_name: None, coordinates: None, + near_current_pane, }) } }, @@ -285,6 +357,7 @@ impl TryFrom<ProtobufAction> for Action { .direction .and_then(|d| ProtobufResizeDirection::from_i32(d)) .and_then(|d| d.try_into().ok()); + let near_current_pane = payload.near_current_pane; if let Some(payload) = payload.command { let pane_name = payload.pane_name.clone(); let run_command_action: RunCommandAction = payload.try_into()?; @@ -292,12 +365,14 @@ impl TryFrom<ProtobufAction> for Action { direction, command: Some(run_command_action), pane_name, + near_current_pane, }) } else { Ok(Action::NewTiledPane { direction, command: None, pane_name: None, + near_current_pane, }) } }, @@ -331,9 +406,77 @@ impl TryFrom<ProtobufAction> for Action { }, Some(ProtobufActionName::NewTab) => { match protobuf_action.optional_payload { - Some(_) => Err("NewTab should not have a payload"), + Some(OptionalPayload::NewTabPayload(payload)) => { + // New behavior: extract all fields from payload + let tiled_layout = + payload.tiled_layout.map(|l| l.try_into()).transpose()?; + + let floating_layouts = payload + .floating_layouts + .into_iter() + .map(|l| l.try_into()) + .collect::<Result<Vec<_>, _>>()?; + + let swap_tiled_layouts = if payload.swap_tiled_layouts.is_empty() { + None + } else { + Some( + payload + .swap_tiled_layouts + .into_iter() + .map(|l| l.try_into()) + .collect::<Result<Vec<_>, _>>()?, + ) + }; + + let swap_floating_layouts = if payload.swap_floating_layouts.is_empty() { + None + } else { + Some( + payload + .swap_floating_layouts + .into_iter() + .map(|l| l.try_into()) + .collect::<Result<Vec<_>, _>>()?, + ) + }; + + let tab_name = payload.tab_name; + let should_change_focus_to_new_tab = payload.should_change_focus_to_new_tab; + let cwd = payload.cwd.map(PathBuf::from); + + let initial_panes = if payload.initial_panes.is_empty() { + None + } else { + Some( + payload + .initial_panes + .into_iter() + .map(|p| p.try_into()) + .collect::<Result<Vec<_>, _>>()?, + ) + }; + + let first_pane_unblock_condition = payload + .first_pane_unblock_condition + .and_then(|uc| ProtobufUnblockCondition::from_i32(uc)) + .and_then(|uc| uc.try_into().ok()); + + Ok(Action::NewTab { + tiled_layout, + floating_layouts, + swap_tiled_layouts, + swap_floating_layouts, + tab_name, + should_change_focus_to_new_tab, + cwd, + initial_panes, + first_pane_unblock_condition, + }) + }, None => { - // we do not serialize the layouts of this action + // Backwards compatibility: accept None payload for existing plugins + // Return the same defaults as before Ok(Action::NewTab { tiled_layout: None, floating_layouts: vec![], @@ -342,8 +485,11 @@ impl TryFrom<ProtobufAction> for Action { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }) }, + _ => Err("Wrong payload for Action::NewTab"), } }, Some(ProtobufActionName::NoOp) => match protobuf_action.optional_payload { @@ -405,6 +551,7 @@ impl TryFrom<ProtobufAction> for Action { let run_command_action = run_command_action.try_into()?; Ok(Action::Run { command: run_command_action, + near_current_pane: false, }) }, _ => Err("Wrong payload for Action::Run"), @@ -657,9 +804,11 @@ impl TryFrom<ProtobufAction> for Action { Some(OptionalPayload::FocusTerminalPaneWithIdPayload(payload)) => { let terminal_pane_id = payload.pane_id; let should_float_if_hidden = payload.should_float; + let should_be_in_place_if_hidden = payload.should_be_in_place; Ok(Action::FocusTerminalPaneWithId { pane_id: terminal_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }) }, _ => Err("Wrong payload for Action::FocusTerminalPaneWithId"), @@ -670,9 +819,11 @@ impl TryFrom<ProtobufAction> for Action { Some(OptionalPayload::FocusPluginPaneWithIdPayload(payload)) => { let plugin_pane_id = payload.pane_id; let should_float_if_hidden = payload.should_float; + let should_be_in_place_if_hidden = payload.should_be_in_place; Ok(Action::FocusPluginPaneWithId { pane_id: plugin_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, }) }, _ => Err("Wrong payload for Action::FocusPluginPaneWithId"), @@ -768,8 +919,60 @@ impl TryFrom<ProtobufAction> for Action { None => Ok(Action::NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }), }, + Some(ProtobufActionName::NewBlockingPane) => match protobuf_action.optional_payload { + Some(OptionalPayload::NewBlockingPanePayload(payload)) => { + let placement: NewPanePlacement = payload + .placement + .ok_or("NewBlockingPanePayload must have a placement")? + .try_into()?; + let pane_name = payload.pane_name; + let command = payload.command.and_then(|c| c.try_into().ok()); + let unblock_condition = payload + .unblock_condition + .and_then(|uc| ProtobufUnblockCondition::from_i32(uc)) + .and_then(|uc| uc.try_into().ok()); + let near_current_pane = payload.near_current_pane; + Ok(Action::NewBlockingPane { + placement, + pane_name, + command, + unblock_condition, + near_current_pane, + }) + }, + _ => Err("Wrong payload for Action::NewBlockingPane"), + }, + Some(ProtobufActionName::NewInPlacePane) => match protobuf_action.optional_payload { + Some(OptionalPayload::NewInPlacePanePayload(payload)) => { + let near_current_pane = payload.near_current_pane; + let pane_id_to_replace = + payload.pane_id_to_replace.and_then(|p| p.try_into().ok()); + let close_replace_pane = payload.close_replace_pane; + if let Some(command) = payload.command { + let pane_name = command.pane_name.clone(); + let run_command_action: RunCommandAction = command.try_into()?; + Ok(Action::NewInPlacePane { + command: Some(run_command_action), + pane_name, + near_current_pane, + pane_id_to_replace, + close_replace_pane, + }) + } else { + Ok(Action::NewInPlacePane { + command: None, + pane_name: payload.pane_name, + near_current_pane, + pane_id_to_replace, + close_replace_pane, + }) + } + }, + _ => Err("Wrong payload for Action::NewInPlacePane"), + }, _ => Err("Unknown Action"), } } @@ -784,15 +987,20 @@ impl TryFrom<Action> for ProtobufAction { optional_payload: None, }), Action::Write { - key_with_modifier: _, + key_with_modifier, bytes, - is_kitty_keyboard_protocol: _, - } => Ok(ProtobufAction { - name: ProtobufActionName::Write as i32, - optional_payload: Some(OptionalPayload::WritePayload(WritePayload { - bytes_to_write: bytes, - })), - }), + is_kitty_keyboard_protocol, + } => { + let protobuf_key_with_modifier = key_with_modifier.and_then(|k| k.try_into().ok()); + Ok(ProtobufAction { + name: ProtobufActionName::Write as i32, + optional_payload: Some(OptionalPayload::WritePayload(WritePayload { + key_with_modifier: protobuf_key_with_modifier, + bytes_to_write: bytes, + is_kitty_keyboard_protocol, + })), + }) + }, Action::WriteChars { chars: chars_to_write, } => Ok(ProtobufAction { @@ -982,6 +1190,7 @@ impl TryFrom<Action> for ProtobufAction { in_place: _should_be_in_place, start_suppressed: _start_suppressed, coordinates: _floating_pane_coordinates, + near_current_pane, } => { let file_to_edit = open_file_payload.path.display().to_string(); let cwd = open_file_payload.cwd.map(|cwd| cwd.display().to_string()); @@ -997,6 +1206,7 @@ impl TryFrom<Action> for ProtobufAction { should_float, direction, cwd, + near_current_pane, })), }) }, @@ -1004,6 +1214,7 @@ impl TryFrom<Action> for ProtobufAction { command: run_command_action, pane_name, coordinates: _coordinates, + near_current_pane, } => { let command = run_command_action.and_then(|r| { let mut protobuf_run_command_action: ProtobufRunCommandAction = @@ -1014,7 +1225,10 @@ impl TryFrom<Action> for ProtobufAction { Ok(ProtobufAction { name: ProtobufActionName::NewFloatingPane as i32, optional_payload: Some(OptionalPayload::NewFloatingPanePayload( - NewFloatingPanePayload { command }, + NewFloatingPanePayload { + command, + near_current_pane, + }, )), }) }, @@ -1022,6 +1236,7 @@ impl TryFrom<Action> for ProtobufAction { direction, command: run_command_action, pane_name, + near_current_pane, } => { let direction = direction.and_then(|direction| { let protobuf_direction: ProtobufResizeDirection = direction.try_into().ok()?; @@ -1037,7 +1252,11 @@ impl TryFrom<Action> for ProtobufAction { Ok(ProtobufAction { name: ProtobufActionName::NewTiledPane as i32, optional_payload: Some(OptionalPayload::NewTiledPanePayload( - NewTiledPanePayload { direction, command }, + NewTiledPanePayload { + direction, + command, + near_current_pane, + }, )), }) }, @@ -1062,18 +1281,82 @@ impl TryFrom<Action> for ProtobufAction { optional_payload: None, }), Action::NewTab { - tiled_layout: _, - floating_layouts: _, - swap_tiled_layouts: _, - swap_floating_layouts: _, - tab_name: _, - should_change_focus_to_new_tab: _, - cwd: _, + tiled_layout, + floating_layouts, + swap_tiled_layouts, + swap_floating_layouts, + tab_name, + should_change_focus_to_new_tab, + cwd, + initial_panes, + first_pane_unblock_condition, } => { - // we do not serialize the various newtab payloads + // Always send payload (even if all fields are default) + let protobuf_tiled_layout = tiled_layout + .as_ref() + .map(|l| l.clone().try_into()) + .transpose()?; + + let protobuf_floating_layouts = floating_layouts + .iter() + .map(|l| l.clone().try_into()) + .collect::<Result<Vec<_>, _>>()?; + + let protobuf_swap_tiled_layouts = swap_tiled_layouts + .as_ref() + .map(|layouts| { + layouts + .iter() + .map(|l| l.clone().try_into()) + .collect::<Result<Vec<_>, _>>() + }) + .transpose()? + .unwrap_or_default(); + + let protobuf_swap_floating_layouts = swap_floating_layouts + .as_ref() + .map(|layouts| { + layouts + .iter() + .map(|l| l.clone().try_into()) + .collect::<Result<Vec<_>, _>>() + }) + .transpose()? + .unwrap_or_default(); + + let cwd_string = cwd.as_ref().map(|p| p.display().to_string()); + + let protobuf_initial_panes = initial_panes + .as_ref() + .map(|panes| { + panes + .iter() + .map(|p| p.clone().try_into()) + .collect::<Result<Vec<_>, _>>() + }) + .transpose()? + .unwrap_or_default(); + + let protobuf_first_pane_unblock_condition = first_pane_unblock_condition + .map(|uc| { + let protobuf_uc: ProtobufUnblockCondition = uc.try_into().ok()?; + Some(protobuf_uc as i32) + }) + .flatten(); + Ok(ProtobufAction { name: ProtobufActionName::NewTab as i32, - optional_payload: None, + optional_payload: Some(OptionalPayload::NewTabPayload(NewTabPayload { + tiled_layout: protobuf_tiled_layout, + floating_layouts: protobuf_floating_layouts, + swap_tiled_layouts: protobuf_swap_tiled_layouts, + swap_floating_layouts: protobuf_swap_floating_layouts, + tab_name: tab_name.clone(), + should_change_focus_to_new_tab, + cwd: cwd_string, + initial_panes: protobuf_initial_panes, + first_pane_unblock_condition: protobuf_first_pane_unblock_condition, + })), }) }, Action::GoToNextTab => Ok(ProtobufAction { @@ -1123,6 +1406,7 @@ impl TryFrom<Action> for ProtobufAction { }, Action::Run { command: run_command_action, + near_current_pane: _, } => { let run_command_action: ProtobufRunCommandAction = run_command_action.try_into()?; Ok(ProtobufAction { @@ -1277,24 +1561,28 @@ impl TryFrom<Action> for ProtobufAction { Action::FocusTerminalPaneWithId { pane_id: terminal_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => Ok(ProtobufAction { name: ProtobufActionName::FocusTerminalPaneWithId as i32, optional_payload: Some(OptionalPayload::FocusTerminalPaneWithIdPayload( PaneIdAndShouldFloat { pane_id: terminal_pane_id, should_float: should_float_if_hidden, + should_be_in_place: should_be_in_place_if_hidden, }, )), }), Action::FocusPluginPaneWithId { pane_id: plugin_pane_id, should_float_if_hidden, + should_be_in_place_if_hidden, } => Ok(ProtobufAction { name: ProtobufActionName::FocusPluginPaneWithId as i32, optional_payload: Some(OptionalPayload::FocusPluginPaneWithIdPayload( PaneIdAndShouldFloat { pane_id: plugin_pane_id, should_float: should_float_if_hidden, + should_be_in_place: should_be_in_place_if_hidden, }, )), }), @@ -1363,16 +1651,72 @@ impl TryFrom<Action> for ProtobufAction { Action::NewStackedPane { command: _, pane_name: _, + near_current_pane: _, } => Ok(ProtobufAction { name: ProtobufActionName::NewStackedPane as i32, optional_payload: None, }), + Action::NewBlockingPane { + placement, + pane_name, + command, + unblock_condition, + near_current_pane, + } => { + let placement: ProtobufNewPanePlacement = placement.try_into()?; + let command = command.and_then(|c| { + let protobuf_command: ProtobufRunCommandAction = c.try_into().ok()?; + Some(protobuf_command) + }); + let unblock_condition = unblock_condition + .map(|uc| { + let protobuf_uc: ProtobufUnblockCondition = uc.try_into().ok()?; + Some(protobuf_uc as i32) + }) + .flatten(); + Ok(ProtobufAction { + name: ProtobufActionName::NewBlockingPane as i32, + optional_payload: Some(OptionalPayload::NewBlockingPanePayload( + NewBlockingPanePayload { + placement: Some(placement), + pane_name, + command, + unblock_condition, + near_current_pane, + }, + )), + }) + }, + Action::NewInPlacePane { + command: run_command_action, + pane_name, + near_current_pane, + pane_id_to_replace, + close_replace_pane, + } => { + let command = run_command_action.and_then(|r| { + let mut protobuf_run_command_action: ProtobufRunCommandAction = + r.try_into().ok()?; + let pane_name = pane_name.and_then(|n| n.try_into().ok()); + protobuf_run_command_action.pane_name = pane_name; + Some(protobuf_run_command_action) + }); + let pane_id_to_replace = pane_id_to_replace.and_then(|p| p.try_into().ok()); + Ok(ProtobufAction { + name: ProtobufActionName::NewInPlacePane as i32, + optional_payload: Some(OptionalPayload::NewInPlacePanePayload( + NewInPlacePanePayload { + command, + pane_name: None, // pane_name is already embedded in command + near_current_pane, + pane_id_to_replace, + close_replace_pane, + }, + )), + }) + }, Action::NoOp | Action::Confirm - | Action::NewInPlacePane { - command: _, - pane_name: _, - } | Action::NewInPlacePluginPane { plugin: _, pane_name: _, @@ -1389,7 +1733,6 @@ impl TryFrom<Action> for ProtobufAction { coordinates: _, } | Action::SkipConfirm { action: _ } - | Action::NewBlockingPane { .. } | Action::SwitchSession { .. } => Err("Unsupported action"), } } @@ -1616,3 +1959,914 @@ impl TryFrom<&ProtobufPluginConfiguration> for BTreeMap<String, String> { Ok(converted) } } + +impl TryFrom<ProtobufKeyWithModifier> for KeyWithModifier { + type Error = &'static str; + fn try_from(protobuf_key: ProtobufKeyWithModifier) -> Result<Self, &'static str> { + let bare_key = match ProtobufBareKey::from_i32(protobuf_key.bare_key) { + Some(ProtobufBareKey::PageDown) => crate::data::BareKey::PageDown, + Some(ProtobufBareKey::PageUp) => crate::data::BareKey::PageUp, + Some(ProtobufBareKey::Left) => crate::data::BareKey::Left, + Some(ProtobufBareKey::Down) => crate::data::BareKey::Down, + Some(ProtobufBareKey::Up) => crate::data::BareKey::Up, + Some(ProtobufBareKey::Right) => crate::data::BareKey::Right, + Some(ProtobufBareKey::Home) => crate::data::BareKey::Home, + Some(ProtobufBareKey::End) => crate::data::BareKey::End, + Some(ProtobufBareKey::Backspace) => crate::data::BareKey::Backspace, + Some(ProtobufBareKey::Delete) => crate::data::BareKey::Delete, + Some(ProtobufBareKey::Insert) => crate::data::BareKey::Insert, + Some(ProtobufBareKey::F1) => crate::data::BareKey::F(1), + Some(ProtobufBareKey::F2) => crate::data::BareKey::F(2), + Some(ProtobufBareKey::F3) => crate::data::BareKey::F(3), + Some(ProtobufBareKey::F4) => crate::data::BareKey::F(4), + Some(ProtobufBareKey::F5) => crate::data::BareKey::F(5), + Some(ProtobufBareKey::F6) => crate::data::BareKey::F(6), + Some(ProtobufBareKey::F7) => crate::data::BareKey::F(7), + Some(ProtobufBareKey::F8) => crate::data::BareKey::F(8), + Some(ProtobufBareKey::F9) => crate::data::BareKey::F(9), + Some(ProtobufBareKey::F10) => crate::data::BareKey::F(10), + Some(ProtobufBareKey::F11) => crate::data::BareKey::F(11), + Some(ProtobufBareKey::F12) => crate::data::BareKey::F(12), + Some(ProtobufBareKey::Char) => { + if let Some(character) = protobuf_key.character { + let ch = character + .chars() + .next() + .ok_or("BareKey::Char requires a character")?; + crate::data::BareKey::Char(ch) + } else { + return Err("BareKey::Char requires a character"); + } + }, + Some(ProtobufBareKey::Tab) => crate::data::BareKey::Tab, + Some(ProtobufBareKey::Esc) => crate::data::BareKey::Esc, + Some(ProtobufBareKey::Enter) => crate::data::BareKey::Enter, + Some(ProtobufBareKey::CapsLock) => crate::data::BareKey::CapsLock, + Some(ProtobufBareKey::ScrollLock) => crate::data::BareKey::ScrollLock, + Some(ProtobufBareKey::NumLock) => crate::data::BareKey::NumLock, + Some(ProtobufBareKey::PrintScreen) => crate::data::BareKey::PrintScreen, + Some(ProtobufBareKey::Pause) => crate::data::BareKey::Pause, + Some(ProtobufBareKey::Menu) => crate::data::BareKey::Menu, + _ => return Err("Unknown BareKey"), + }; + + let mut key_modifiers = std::collections::BTreeSet::new(); + for modifier in protobuf_key.key_modifiers { + let key_modifier = match ProtobufKeyModifier::from_i32(modifier) { + Some(ProtobufKeyModifier::Ctrl) => crate::data::KeyModifier::Ctrl, + Some(ProtobufKeyModifier::Alt) => crate::data::KeyModifier::Alt, + Some(ProtobufKeyModifier::Shift) => crate::data::KeyModifier::Shift, + Some(ProtobufKeyModifier::Super) => crate::data::KeyModifier::Super, + _ => continue, + }; + key_modifiers.insert(key_modifier); + } + + Ok(KeyWithModifier { + bare_key, + key_modifiers, + }) + } +} + +impl TryFrom<KeyWithModifier> for ProtobufKeyWithModifier { + type Error = &'static str; + fn try_from(key: KeyWithModifier) -> Result<Self, &'static str> { + let (bare_key, character) = match key.bare_key { + crate::data::BareKey::PageDown => (ProtobufBareKey::PageDown as i32, None), + crate::data::BareKey::PageUp => (ProtobufBareKey::PageUp as i32, None), + crate::data::BareKey::Left => (ProtobufBareKey::Left as i32, None), + crate::data::BareKey::Down => (ProtobufBareKey::Down as i32, None), + crate::data::BareKey::Up => (ProtobufBareKey::Up as i32, None), + crate::data::BareKey::Right => (ProtobufBareKey::Right as i32, None), + crate::data::BareKey::Home => (ProtobufBareKey::Home as i32, None), + crate::data::BareKey::End => (ProtobufBareKey::End as i32, None), + crate::data::BareKey::Backspace => (ProtobufBareKey::Backspace as i32, None), + crate::data::BareKey::Delete => (ProtobufBareKey::Delete as i32, None), + crate::data::BareKey::Insert => (ProtobufBareKey::Insert as i32, None), + crate::data::BareKey::F(1) => (ProtobufBareKey::F1 as i32, None), + crate::data::BareKey::F(2) => (ProtobufBareKey::F2 as i32, None), + crate::data::BareKey::F(3) => (ProtobufBareKey::F3 as i32, None), + crate::data::BareKey::F(4) => (ProtobufBareKey::F4 as i32, None), + crate::data::BareKey::F(5) => (ProtobufBareKey::F5 as i32, None), + crate::data::BareKey::F(6) => (ProtobufBareKey::F6 as i32, None), + crate::data::BareKey::F(7) => (ProtobufBareKey::F7 as i32, None), + crate::data::BareKey::F(8) => (ProtobufBareKey::F8 as i32, None), + crate::data::BareKey::F(9) => (ProtobufBareKey::F9 as i32, None), + crate::data::BareKey::F(10) => (ProtobufBareKey::F10 as i32, None), + crate::data::BareKey::F(11) => (ProtobufBareKey::F11 as i32, None), + crate::data::BareKey::F(12) => (ProtobufBareKey::F12 as i32, None), + crate::data::BareKey::Char(c) => (ProtobufBareKey::Char as i32, Some(c.to_string())), + crate::data::BareKey::Tab => (ProtobufBareKey::Tab as i32, None), + crate::data::BareKey::Esc => (ProtobufBareKey::Esc as i32, None), + crate::data::BareKey::Enter => (ProtobufBareKey::Enter as i32, None), + crate::data::BareKey::CapsLock => (ProtobufBareKey::CapsLock as i32, None), + crate::data::BareKey::ScrollLock => (ProtobufBareKey::ScrollLock as i32, None), + crate::data::BareKey::NumLock => (ProtobufBareKey::NumLock as i32, None), + crate::data::BareKey::PrintScreen => (ProtobufBareKey::PrintScreen as i32, None), + crate::data::BareKey::Pause => (ProtobufBareKey::Pause as i32, None), + crate::data::BareKey::Menu => (ProtobufBareKey::Menu as i32, None), + _ => return Err("Unsupported BareKey"), + }; + + let key_modifiers: Vec<i32> = key + .key_modifiers + .iter() + .map(|m| match m { + crate::data::KeyModifier::Ctrl => ProtobufKeyModifier::Ctrl as i32, + crate::data::KeyModifier::Alt => ProtobufKeyModifier::Alt as i32, + crate::data::KeyModifier::Shift => ProtobufKeyModifier::Shift as i32, + crate::data::KeyModifier::Super => ProtobufKeyModifier::Super as i32, + }) + .collect(); + + Ok(ProtobufKeyWithModifier { + bare_key, + key_modifiers, + character, + }) + } +} + +// UnblockCondition conversions +impl TryFrom<ProtobufUnblockCondition> for UnblockCondition { + type Error = &'static str; + fn try_from(protobuf_uc: ProtobufUnblockCondition) -> Result<Self, &'static str> { + match protobuf_uc { + ProtobufUnblockCondition::UnblockOnExitSuccess => Ok(UnblockCondition::OnExitSuccess), + ProtobufUnblockCondition::UnblockOnExitFailure => Ok(UnblockCondition::OnExitFailure), + ProtobufUnblockCondition::UnblockOnAnyExit => Ok(UnblockCondition::OnAnyExit), + } + } +} + +impl TryFrom<UnblockCondition> for ProtobufUnblockCondition { + type Error = &'static str; + fn try_from(uc: UnblockCondition) -> Result<Self, &'static str> { + match uc { + UnblockCondition::OnExitSuccess => Ok(ProtobufUnblockCondition::UnblockOnExitSuccess), + UnblockCondition::OnExitFailure => Ok(ProtobufUnblockCondition::UnblockOnExitFailure), + UnblockCondition::OnAnyExit => Ok(ProtobufUnblockCondition::UnblockOnAnyExit), + } + } +} + +impl TryFrom<i32> for UnblockCondition { + type Error = &'static str; + fn try_from(value: i32) -> Result<Self, &'static str> { + match ProtobufUnblockCondition::from_i32(value) { + Some(uc) => uc.try_into(), + None => Err("Invalid UnblockCondition value"), + } + } +} + +// PaneId conversions +impl TryFrom<ProtobufPaneId> for PaneId { + type Error = &'static str; + fn try_from(protobuf_pane_id: ProtobufPaneId) -> Result<Self, &'static str> { + use super::generated_api::api::action::pane_id::PaneIdVariant; + + match protobuf_pane_id.pane_id_variant { + Some(PaneIdVariant::Terminal(id)) => Ok(PaneId::Terminal(id)), + Some(PaneIdVariant::Plugin(id)) => Ok(PaneId::Plugin(id)), + None => Err("PaneId must have either terminal or plugin id"), + } + } +} + +impl TryFrom<PaneId> for ProtobufPaneId { + type Error = &'static str; + fn try_from(pane_id: PaneId) -> Result<Self, &'static str> { + use super::generated_api::api::action::pane_id::PaneIdVariant; + + let pane_id_variant = match pane_id { + PaneId::Terminal(id) => Some(PaneIdVariant::Terminal(id)), + PaneId::Plugin(id) => Some(PaneIdVariant::Plugin(id)), + }; + Ok(ProtobufPaneId { pane_id_variant }) + } +} + +// SplitSize conversions +impl TryFrom<ProtobufSplitSize> for SplitSize { + type Error = &'static str; + fn try_from(protobuf_split_size: ProtobufSplitSize) -> Result<Self, &'static str> { + use super::generated_api::api::action::split_size::SplitSizeVariant; + + match protobuf_split_size.split_size_variant { + Some(SplitSizeVariant::Percent(p)) => Ok(SplitSize::Percent(p as usize)), + Some(SplitSizeVariant::Fixed(f)) => Ok(SplitSize::Fixed(f as usize)), + None => Err("SplitSize must have either percent or fixed value"), + } + } +} + +impl TryFrom<SplitSize> for ProtobufSplitSize { + type Error = &'static str; + fn try_from(split_size: SplitSize) -> Result<Self, &'static str> { + use super::generated_api::api::action::split_size::SplitSizeVariant; + + let split_size_variant = match split_size { + SplitSize::Percent(p) => Some(SplitSizeVariant::Percent(p as u32)), + SplitSize::Fixed(f) => Some(SplitSizeVariant::Fixed(f as u32)), + }; + Ok(ProtobufSplitSize { split_size_variant }) + } +} + +// FloatingPaneCoordinates conversions +impl TryFrom<ProtobufFloatingPaneCoordinates> for FloatingPaneCoordinates { + type Error = &'static str; + fn try_from(protobuf_coords: ProtobufFloatingPaneCoordinates) -> Result<Self, &'static str> { + Ok(FloatingPaneCoordinates { + x: protobuf_coords.x.and_then(|x| x.try_into().ok()), + y: protobuf_coords.y.and_then(|y| y.try_into().ok()), + width: protobuf_coords.width.and_then(|w| w.try_into().ok()), + height: protobuf_coords.height.and_then(|h| h.try_into().ok()), + pinned: protobuf_coords.pinned, + }) + } +} + +impl TryFrom<FloatingPaneCoordinates> for ProtobufFloatingPaneCoordinates { + type Error = &'static str; + fn try_from(coords: FloatingPaneCoordinates) -> Result<Self, &'static str> { + Ok(ProtobufFloatingPaneCoordinates { + x: coords.x.and_then(|x| x.try_into().ok()), + y: coords.y.and_then(|y| y.try_into().ok()), + width: coords.width.and_then(|w| w.try_into().ok()), + height: coords.height.and_then(|h| h.try_into().ok()), + pinned: coords.pinned, + }) + } +} + +// NewPanePlacement conversions +impl TryFrom<ProtobufNewPanePlacement> for NewPanePlacement { + type Error = &'static str; + fn try_from(protobuf_placement: ProtobufNewPanePlacement) -> Result<Self, &'static str> { + use super::generated_api::api::action::new_pane_placement::PlacementVariant; + + match protobuf_placement.placement_variant { + Some(PlacementVariant::NoPreference(_)) => Ok(NewPanePlacement::NoPreference), + Some(PlacementVariant::Tiled(tiled)) => { + let direction = tiled + .direction + .and_then(|d| ProtobufResizeDirection::from_i32(d)) + .and_then(|d| d.try_into().ok()); + Ok(NewPanePlacement::Tiled(direction)) + }, + Some(PlacementVariant::Floating(floating)) => { + let coords = floating.coordinates.and_then(|c| c.try_into().ok()); + Ok(NewPanePlacement::Floating(coords)) + }, + Some(PlacementVariant::InPlace(config)) => { + let pane_id_to_replace = + config.pane_id_to_replace.and_then(|id| id.try_into().ok()); + Ok(NewPanePlacement::InPlace { + pane_id_to_replace, + close_replaced_pane: config.close_replaced_pane, + }) + }, + Some(PlacementVariant::Stacked(stacked)) => { + let pane_id = stacked.pane_id.and_then(|id| id.try_into().ok()); + Ok(NewPanePlacement::Stacked(pane_id)) + }, + None => Err("NewPanePlacement must have a placement variant"), + } + } +} + +impl TryFrom<NewPanePlacement> for ProtobufNewPanePlacement { + type Error = &'static str; + fn try_from(placement: NewPanePlacement) -> Result<Self, &'static str> { + use super::generated_api::api::action::new_pane_placement::PlacementVariant; + + let placement_variant = match placement { + NewPanePlacement::NoPreference => Some(PlacementVariant::NoPreference(true)), + NewPanePlacement::Tiled(direction) => { + let direction = direction.and_then(|d| { + let protobuf_direction: ProtobufResizeDirection = d.try_into().ok()?; + Some(protobuf_direction as i32) + }); + Some(PlacementVariant::Tiled(ProtobufTiledPlacement { + direction, + })) + }, + NewPanePlacement::Floating(coords) => { + let coordinates = coords.and_then(|c| c.try_into().ok()); + Some(PlacementVariant::Floating(ProtobufFloatingPlacement { + coordinates, + })) + }, + NewPanePlacement::InPlace { + pane_id_to_replace, + close_replaced_pane, + } => { + let pane_id_to_replace = pane_id_to_replace.and_then(|id| id.try_into().ok()); + Some(PlacementVariant::InPlace(ProtobufInPlaceConfig { + pane_id_to_replace, + close_replaced_pane, + })) + }, + NewPanePlacement::Stacked(pane_id) => { + let pane_id = pane_id.and_then(|id| id.try_into().ok()); + Some(PlacementVariant::Stacked(ProtobufStackedPlacement { + pane_id, + })) + }, + }; + + Ok(ProtobufNewPanePlacement { placement_variant }) + } +} + +// Layout type conversions + +impl TryFrom<ProtobufPercentOrFixed> for PercentOrFixed { + type Error = &'static str; + fn try_from(protobuf: ProtobufPercentOrFixed) -> Result<Self, Self::Error> { + use super::generated_api::api::action::percent_or_fixed::SizeType; + match protobuf.size_type { + Some(SizeType::Percent(p)) => Ok(PercentOrFixed::Percent(p as usize)), + Some(SizeType::Fixed(f)) => Ok(PercentOrFixed::Fixed(f as usize)), + None => Err("PercentOrFixed must have a size_type"), + } + } +} + +impl TryFrom<PercentOrFixed> for ProtobufPercentOrFixed { + type Error = &'static str; + fn try_from(internal: PercentOrFixed) -> Result<Self, Self::Error> { + use super::generated_api::api::action::percent_or_fixed::SizeType; + let size_type = match internal { + PercentOrFixed::Percent(p) => Some(SizeType::Percent(p as u32)), + PercentOrFixed::Fixed(f) => Some(SizeType::Fixed(f as u32)), + }; + Ok(ProtobufPercentOrFixed { size_type }) + } +} + +impl TryFrom<ProtobufSplitDirection> for SplitDirection { + type Error = &'static str; + fn try_from(protobuf: ProtobufSplitDirection) -> Result<Self, Self::Error> { + match protobuf { + ProtobufSplitDirection::Horizontal => Ok(SplitDirection::Horizontal), + ProtobufSplitDirection::Vertical => Ok(SplitDirection::Vertical), + ProtobufSplitDirection::Unspecified => Err("SplitDirection cannot be unspecified"), + } + } +} + +impl TryFrom<SplitDirection> for ProtobufSplitDirection { + type Error = &'static str; + fn try_from(internal: SplitDirection) -> Result<Self, Self::Error> { + Ok(match internal { + SplitDirection::Horizontal => ProtobufSplitDirection::Horizontal, + SplitDirection::Vertical => ProtobufSplitDirection::Vertical, + }) + } +} + +impl TryFrom<ProtobufLayoutConstraint> for LayoutConstraint { + type Error = &'static str; + fn try_from(protobuf: ProtobufLayoutConstraint) -> Result<Self, Self::Error> { + match protobuf { + ProtobufLayoutConstraint::MaxPanes => Ok(LayoutConstraint::MaxPanes(0)), + ProtobufLayoutConstraint::MinPanes => Ok(LayoutConstraint::MinPanes(0)), + ProtobufLayoutConstraint::ExactPanes => Ok(LayoutConstraint::ExactPanes(0)), + ProtobufLayoutConstraint::NoConstraint => Ok(LayoutConstraint::NoConstraint), + ProtobufLayoutConstraint::Unspecified => Err("LayoutConstraint cannot be unspecified"), + } + } +} + +impl TryFrom<LayoutConstraint> for ProtobufLayoutConstraint { + type Error = &'static str; + fn try_from(internal: LayoutConstraint) -> Result<Self, Self::Error> { + Ok(match internal { + LayoutConstraint::MaxPanes(_) => ProtobufLayoutConstraint::MaxPanes, + LayoutConstraint::MinPanes(_) => ProtobufLayoutConstraint::MinPanes, + LayoutConstraint::ExactPanes(_) => ProtobufLayoutConstraint::ExactPanes, + LayoutConstraint::NoConstraint => ProtobufLayoutConstraint::NoConstraint, + }) + } +} + +impl TryFrom<ProtobufLayoutConstraintWithValue> for LayoutConstraint { + type Error = &'static str; + fn try_from(protobuf: ProtobufLayoutConstraintWithValue) -> Result<Self, Self::Error> { + let constraint_type = ProtobufLayoutConstraint::from_i32(protobuf.constraint_type) + .ok_or("Invalid constraint type")?; + match constraint_type { + ProtobufLayoutConstraint::MaxPanes => Ok(LayoutConstraint::MaxPanes( + protobuf.value.unwrap_or(0) as usize, + )), + ProtobufLayoutConstraint::MinPanes => Ok(LayoutConstraint::MinPanes( + protobuf.value.unwrap_or(0) as usize, + )), + ProtobufLayoutConstraint::ExactPanes => Ok(LayoutConstraint::ExactPanes( + protobuf.value.unwrap_or(0) as usize, + )), + ProtobufLayoutConstraint::NoConstraint => Ok(LayoutConstraint::NoConstraint), + ProtobufLayoutConstraint::Unspecified => Err("LayoutConstraint cannot be unspecified"), + } + } +} + +impl TryFrom<LayoutConstraint> for ProtobufLayoutConstraintWithValue { + type Error = &'static str; + fn try_from(internal: LayoutConstraint) -> Result<Self, Self::Error> { + let (constraint_type, value) = match internal { + LayoutConstraint::MaxPanes(v) => { + (ProtobufLayoutConstraint::MaxPanes as i32, Some(v as u32)) + }, + LayoutConstraint::MinPanes(v) => { + (ProtobufLayoutConstraint::MinPanes as i32, Some(v as u32)) + }, + LayoutConstraint::ExactPanes(v) => { + (ProtobufLayoutConstraint::ExactPanes as i32, Some(v as u32)) + }, + LayoutConstraint::NoConstraint => (ProtobufLayoutConstraint::NoConstraint as i32, None), + }; + Ok(ProtobufLayoutConstraintWithValue { + constraint_type, + value, + }) + } +} + +impl TryFrom<ProtobufPluginTag> for PluginTag { + type Error = &'static str; + fn try_from(protobuf: ProtobufPluginTag) -> Result<Self, Self::Error> { + Ok(PluginTag::new(protobuf.tag)) + } +} + +impl TryFrom<PluginTag> for ProtobufPluginTag { + type Error = &'static str; + fn try_from(internal: PluginTag) -> Result<Self, Self::Error> { + Ok(ProtobufPluginTag { + tag: internal.into(), + }) + } +} + +impl TryFrom<ProtobufPluginUserConfiguration> for PluginUserConfiguration { + type Error = &'static str; + fn try_from(protobuf: ProtobufPluginUserConfiguration) -> Result<Self, Self::Error> { + let btree_map: BTreeMap<String, String> = protobuf.configuration.into_iter().collect(); + Ok(PluginUserConfiguration::new(btree_map)) + } +} + +impl TryFrom<PluginUserConfiguration> for ProtobufPluginUserConfiguration { + type Error = &'static str; + fn try_from(internal: PluginUserConfiguration) -> Result<Self, Self::Error> { + let configuration = internal.inner().clone().into_iter().collect(); + Ok(ProtobufPluginUserConfiguration { configuration }) + } +} + +impl TryFrom<ProtobufRunPluginLocationData> for RunPluginLocation { + type Error = &'static str; + fn try_from(protobuf: ProtobufRunPluginLocationData) -> Result<Self, Self::Error> { + use super::generated_api::api::action::run_plugin_location_data::LocationData; + match protobuf.location_data { + Some(LocationData::FilePath(path)) => Ok(RunPluginLocation::File(PathBuf::from(path))), + Some(LocationData::ZellijTag(tag)) => Ok(RunPluginLocation::Zellij(tag.try_into()?)), + Some(LocationData::RemoteUrl(url)) => Ok(RunPluginLocation::Remote(url)), + None => Err("RunPluginLocationData must have location_data"), + } + } +} + +impl TryFrom<RunPluginLocation> for ProtobufRunPluginLocationData { + type Error = &'static str; + fn try_from(internal: RunPluginLocation) -> Result<Self, Self::Error> { + use super::generated_api::api::action::{ + run_plugin_location_data::LocationData, + RunPluginLocation as ProtobufRunPluginLocationType, + }; + let (location_type, location_data) = match internal { + RunPluginLocation::File(path) => ( + ProtobufRunPluginLocationType::File as i32, + Some(LocationData::FilePath(path.display().to_string())), + ), + RunPluginLocation::Zellij(tag) => ( + ProtobufRunPluginLocationType::Zellij as i32, + Some(LocationData::ZellijTag(tag.try_into()?)), + ), + RunPluginLocation::Remote(url) => ( + ProtobufRunPluginLocationType::Remote as i32, + Some(LocationData::RemoteUrl(url)), + ), + }; + Ok(ProtobufRunPluginLocationData { + location_type, + location_data, + }) + } +} + +impl TryFrom<ProtobufRunPlugin> for RunPlugin { + type Error = &'static str; + fn try_from(protobuf: ProtobufRunPlugin) -> Result<Self, Self::Error> { + let location = protobuf + .location + .ok_or("RunPlugin must have location")? + .try_into()?; + let configuration = protobuf + .configuration + .ok_or("RunPlugin must have configuration")? + .try_into()?; + let initial_cwd = protobuf.initial_cwd.map(PathBuf::from); + Ok(RunPlugin { + _allow_exec_host_cmd: protobuf.allow_exec_host_cmd, + location, + configuration, + initial_cwd, + }) + } +} + +impl TryFrom<RunPlugin> for ProtobufRunPlugin { + type Error = &'static str; + fn try_from(internal: RunPlugin) -> Result<Self, Self::Error> { + Ok(ProtobufRunPlugin { + allow_exec_host_cmd: internal._allow_exec_host_cmd, + location: Some(internal.location.try_into()?), + configuration: Some(internal.configuration.try_into()?), + initial_cwd: internal.initial_cwd.map(|p| p.display().to_string()), + }) + } +} + +impl TryFrom<ProtobufPluginAlias> for PluginAlias { + type Error = &'static str; + fn try_from(protobuf: ProtobufPluginAlias) -> Result<Self, Self::Error> { + let configuration = protobuf.configuration.map(|c| c.try_into()).transpose()?; + let initial_cwd = protobuf.initial_cwd.map(PathBuf::from); + let run_plugin = protobuf.run_plugin.map(|r| r.try_into()).transpose()?; + Ok(PluginAlias { + name: protobuf.name, + configuration, + initial_cwd, + run_plugin, + }) + } +} + +impl TryFrom<PluginAlias> for ProtobufPluginAlias { + type Error = &'static str; + fn try_from(internal: PluginAlias) -> Result<Self, Self::Error> { + Ok(ProtobufPluginAlias { + name: internal.name, + configuration: internal.configuration.map(|c| c.try_into()).transpose()?, + initial_cwd: internal.initial_cwd.map(|p| p.display().to_string()), + run_plugin: internal.run_plugin.map(|r| r.try_into()).transpose()?, + }) + } +} + +impl TryFrom<ProtobufRunPluginOrAlias> for RunPluginOrAlias { + type Error = &'static str; + fn try_from(protobuf: ProtobufRunPluginOrAlias) -> Result<Self, Self::Error> { + use super::generated_api::api::action::run_plugin_or_alias::PluginType; + match protobuf.plugin_type { + Some(PluginType::Plugin(plugin)) => Ok(RunPluginOrAlias::RunPlugin(plugin.try_into()?)), + Some(PluginType::Alias(alias)) => Ok(RunPluginOrAlias::Alias(alias.try_into()?)), + None => Err("RunPluginOrAlias must have plugin_type"), + } + } +} + +impl TryFrom<RunPluginOrAlias> for ProtobufRunPluginOrAlias { + type Error = &'static str; + fn try_from(internal: RunPluginOrAlias) -> Result<Self, Self::Error> { + use super::generated_api::api::action::run_plugin_or_alias::PluginType; + let plugin_type = match internal { + RunPluginOrAlias::RunPlugin(plugin) => Some(PluginType::Plugin(plugin.try_into()?)), + RunPluginOrAlias::Alias(alias) => Some(PluginType::Alias(alias.try_into()?)), + }; + Ok(ProtobufRunPluginOrAlias { plugin_type }) + } +} + +impl TryFrom<ProtobufRunEditFileAction> for Run { + type Error = &'static str; + fn try_from(protobuf: ProtobufRunEditFileAction) -> Result<Self, Self::Error> { + let file_path = PathBuf::from(protobuf.file_path); + let line_number = protobuf.line_number.map(|n| n as usize); + let cwd = protobuf.cwd.map(PathBuf::from); + Ok(Run::EditFile(file_path, line_number, cwd)) + } +} + +impl TryFrom<ProtobufPaneRun> for Run { + type Error = &'static str; + fn try_from(protobuf: ProtobufPaneRun) -> Result<Self, Self::Error> { + use super::generated_api::api::action::pane_run::RunType; + use crate::input::command::RunCommand; + match protobuf.run_type { + Some(RunType::Command(cmd)) => { + let run_command_action: RunCommandAction = cmd.try_into()?; + let run_command: RunCommand = run_command_action.into(); + Ok(Run::Command(run_command)) + }, + Some(RunType::Plugin(plugin)) => Ok(Run::Plugin(plugin.try_into()?)), + Some(RunType::EditFile(edit)) => edit.try_into(), + Some(RunType::Cwd(cwd)) => Ok(Run::Cwd(PathBuf::from(cwd))), + None => Err("PaneRun must have run_type"), + } + } +} + +impl TryFrom<Run> for ProtobufPaneRun { + type Error = &'static str; + fn try_from(internal: Run) -> Result<Self, Self::Error> { + use super::generated_api::api::action::pane_run::RunType; + let run_type = match internal { + Run::Command(cmd) => { + let run_command_action: RunCommandAction = cmd.into(); + Some(RunType::Command(run_command_action.try_into()?)) + }, + Run::Plugin(plugin) => Some(RunType::Plugin(plugin.try_into()?)), + Run::EditFile(file_path, line_number, cwd) => { + Some(RunType::EditFile(ProtobufRunEditFileAction { + file_path: file_path.display().to_string(), + line_number: line_number.map(|n| n as u32), + cwd: cwd.map(|p| p.display().to_string()), + })) + }, + Run::Cwd(cwd) => Some(RunType::Cwd(cwd.display().to_string())), + }; + Ok(ProtobufPaneRun { run_type }) + } +} + +impl TryFrom<ProtobufCommandOrPlugin> for CommandOrPlugin { + type Error = &'static str; + fn try_from(protobuf: ProtobufCommandOrPlugin) -> Result<Self, Self::Error> { + use super::generated_api::api::action::command_or_plugin::CommandOrPluginType; + match protobuf.command_or_plugin_type { + Some(CommandOrPluginType::Command(cmd)) => { + Ok(CommandOrPlugin::Command(cmd.try_into()?)) + }, + Some(CommandOrPluginType::Plugin(plugin)) => { + Ok(CommandOrPlugin::Plugin(plugin.try_into()?)) + }, + None => Err("CommandOrPlugin must have command_or_plugin_type"), + } + } +} + +impl TryFrom<CommandOrPlugin> for ProtobufCommandOrPlugin { + type Error = &'static str; + fn try_from(internal: CommandOrPlugin) -> Result<Self, Self::Error> { + use super::generated_api::api::action::command_or_plugin::CommandOrPluginType; + let command_or_plugin_type = match internal { + CommandOrPlugin::Command(cmd) => Some(CommandOrPluginType::Command(cmd.try_into()?)), + CommandOrPlugin::Plugin(plugin) => { + Some(CommandOrPluginType::Plugin(plugin.try_into()?)) + }, + }; + Ok(ProtobufCommandOrPlugin { + command_or_plugin_type, + }) + } +} + +impl TryFrom<ProtobufTiledPaneLayout> for TiledPaneLayout { + type Error = &'static str; + fn try_from(protobuf: ProtobufTiledPaneLayout) -> Result<Self, Self::Error> { + let children_split_direction = + ProtobufSplitDirection::from_i32(protobuf.children_split_direction) + .ok_or("Invalid split direction")? + .try_into()?; + let children = protobuf + .children + .into_iter() + .map(|c| c.try_into()) + .collect::<Result<Vec<_>, _>>()?; + let split_size = protobuf.split_size.map(|s| s.try_into()).transpose()?; + let run = protobuf.run.map(|r| r.try_into()).transpose()?; + let focus = protobuf.focus.and_then(|f| { + if f == "true" { + Some(true) + } else if f == "false" { + Some(false) + } else { + None + } + }); + let run_instructions_to_ignore = vec![]; // Not serialized in protobuf + Ok(TiledPaneLayout { + children_split_direction, + name: protobuf.name, + children, + split_size, + run, + borderless: protobuf.borderless, + focus, + external_children_index: protobuf.external_children_index.map(|i| i as usize), + children_are_stacked: protobuf.children_are_stacked, + is_expanded_in_stack: protobuf.is_expanded_in_stack, + exclude_from_sync: protobuf.exclude_from_sync, + run_instructions_to_ignore, + hide_floating_panes: protobuf.hide_floating_panes, + pane_initial_contents: protobuf.pane_initial_contents, + }) + } +} + +impl TryFrom<TiledPaneLayout> for ProtobufTiledPaneLayout { + type Error = &'static str; + fn try_from(internal: TiledPaneLayout) -> Result<Self, Self::Error> { + let children_split_direction: ProtobufSplitDirection = + internal.children_split_direction.try_into()?; + let children = internal + .children + .into_iter() + .map(|c| c.try_into()) + .collect::<Result<Vec<_>, _>>()?; + let split_size = internal.split_size.map(|s| s.try_into()).transpose()?; + let run = internal.run.map(|r| r.try_into()).transpose()?; + let focus = internal.focus.map(|f| f.to_string()); + Ok(ProtobufTiledPaneLayout { + children_split_direction: children_split_direction as i32, + name: internal.name, + children, + split_size, + run, + borderless: internal.borderless, + focus, + external_children_index: internal.external_children_index.map(|i| i as u32), + children_are_stacked: internal.children_are_stacked, + is_expanded_in_stack: internal.is_expanded_in_stack, + exclude_from_sync: internal.exclude_from_sync, + hide_floating_panes: internal.hide_floating_panes, + pane_initial_contents: internal.pane_initial_contents, + }) + } +} + +impl TryFrom<ProtobufFloatingPaneLayout> for FloatingPaneLayout { + type Error = &'static str; + fn try_from(protobuf: ProtobufFloatingPaneLayout) -> Result<Self, Self::Error> { + let height = protobuf.height.map(|h| h.try_into()).transpose()?; + let width = protobuf.width.map(|w| w.try_into()).transpose()?; + let x = protobuf.x.map(|x| x.try_into()).transpose()?; + let y = protobuf.y.map(|y| y.try_into()).transpose()?; + let run = protobuf.run.map(|r| r.try_into()).transpose()?; + Ok(FloatingPaneLayout { + name: protobuf.name, + height, + width, + x, + y, + pinned: protobuf.pinned, + run, + focus: protobuf.focus, + already_running: protobuf.already_running, + pane_initial_contents: protobuf.pane_initial_contents, + logical_position: protobuf.logical_position.map(|p| p as usize), + }) + } +} + +impl TryFrom<FloatingPaneLayout> for ProtobufFloatingPaneLayout { + type Error = &'static str; + fn try_from(internal: FloatingPaneLayout) -> Result<Self, Self::Error> { + let height = internal.height.map(|h| h.try_into()).transpose()?; + let width = internal.width.map(|w| w.try_into()).transpose()?; + let x = internal.x.map(|x| x.try_into()).transpose()?; + let y = internal.y.map(|y| y.try_into()).transpose()?; + let run = internal.run.map(|r| r.try_into()).transpose()?; + Ok(ProtobufFloatingPaneLayout { + name: internal.name, + height, + width, + x, + y, + pinned: internal.pinned, + run, + focus: internal.focus, + already_running: internal.already_running, + pane_initial_contents: internal.pane_initial_contents, + logical_position: internal.logical_position.map(|p| p as u32), + }) + } +} + +impl TryFrom<ProtobufLayoutConstraintTiledPair> for (LayoutConstraint, TiledPaneLayout) { + type Error = &'static str; + fn try_from(protobuf: ProtobufLayoutConstraintTiledPair) -> Result<Self, Self::Error> { + let constraint = protobuf + .constraint + .ok_or("LayoutConstraintTiledPair must have constraint")? + .try_into()?; + let layout = protobuf + .layout + .ok_or("LayoutConstraintTiledPair must have layout")? + .try_into()?; + Ok((constraint, layout)) + } +} + +impl TryFrom<(LayoutConstraint, TiledPaneLayout)> for ProtobufLayoutConstraintTiledPair { + type Error = &'static str; + fn try_from(internal: (LayoutConstraint, TiledPaneLayout)) -> Result<Self, Self::Error> { + Ok(ProtobufLayoutConstraintTiledPair { + constraint: Some(internal.0.try_into()?), + layout: Some(internal.1.try_into()?), + }) + } +} + +impl TryFrom<ProtobufLayoutConstraintFloatingPair> for (LayoutConstraint, Vec<FloatingPaneLayout>) { + type Error = &'static str; + fn try_from(protobuf: ProtobufLayoutConstraintFloatingPair) -> Result<Self, Self::Error> { + let constraint = protobuf + .constraint + .ok_or("LayoutConstraintFloatingPair must have constraint")? + .try_into()?; + let layouts = protobuf + .layouts + .into_iter() + .map(|l| l.try_into()) + .collect::<Result<Vec<_>, _>>()?; + Ok((constraint, layouts)) + } +} + +impl TryFrom<(LayoutConstraint, Vec<FloatingPaneLayout>)> for ProtobufLayoutConstraintFloatingPair { + type Error = &'static str; + fn try_from( + internal: (LayoutConstraint, Vec<FloatingPaneLayout>), + ) -> Result<Self, Self::Error> { + Ok(ProtobufLayoutConstraintFloatingPair { + constraint: Some(internal.0.try_into()?), + layouts: internal + .1 + .into_iter() + .map(|l| l.try_into()) + .collect::<Result<Vec<_>, _>>()?, + }) + } +} + +impl TryFrom<ProtobufSwapTiledLayout> for SwapTiledLayout { + type Error = &'static str; + fn try_from(protobuf: ProtobufSwapTiledLayout) -> Result<Self, Self::Error> { + let constraint_map: BTreeMap<LayoutConstraint, TiledPaneLayout> = protobuf + .constraint_map + .into_iter() + .map(|pair| pair.try_into()) + .collect::<Result<BTreeMap<_, _>, _>>()?; + Ok((constraint_map, protobuf.name)) + } +} + +impl TryFrom<SwapTiledLayout> for ProtobufSwapTiledLayout { + type Error = &'static str; + fn try_from(internal: SwapTiledLayout) -> Result<Self, Self::Error> { + let constraint_map = internal + .0 + .into_iter() + .map(|(constraint, layout)| (constraint, layout).try_into()) + .collect::<Result<Vec<_>, _>>()?; + Ok(ProtobufSwapTiledLayout { + constraint_map, + name: internal.1, + }) + } +} + +impl TryFrom<ProtobufSwapFloatingLayout> for SwapFloatingLayout { + type Error = &'static str; + fn try_from(protobuf: ProtobufSwapFloatingLayout) -> Result<Self, Self::Error> { + let constraint_map: BTreeMap<LayoutConstraint, Vec<FloatingPaneLayout>> = protobuf + .constraint_map + .into_iter() + .map(|pair| pair.try_into()) + .collect::<Result<BTreeMap<_, _>, _>>()?; + Ok((constraint_map, protobuf.name)) + } +} + +impl TryFrom<SwapFloatingLayout> for ProtobufSwapFloatingLayout { + type Error = &'static str; + fn try_from(internal: SwapFloatingLayout) -> Result<Self, Self::Error> { + let constraint_map = internal + .0 + .into_iter() + .map(|(constraint, layouts)| (constraint, layouts).try_into()) + .collect::<Result<Vec<_>, _>>()?; + Ok(ProtobufSwapFloatingLayout { + constraint_map, + name: internal.1, + }) + } +} diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index f5059131..3bdeea19 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -59,6 +59,9 @@ enum EventType { FailedToStartWebServer = 34; InterceptedKeyPress = 35; PaneRenderReport = 36; + UserAction = 37; + ActionComplete = 38; + CwdChanged = 39; } message EventNameList { @@ -97,9 +100,18 @@ message Event { FailedToStartWebServerPayload failed_to_start_web_server_payload = 28; key.Key intercepted_key_payload = 29; PaneRenderReportPayload pane_render_report_payload = 30; + UserActionPayload user_action_payload = 31; + ActionCompletePayload action_complete_payload = 32; + CwdChangedPayload cwd_changed_payload = 33; } } +message CwdChangedPayload { + PaneId pane_id = 1; + string new_cwd = 2; + repeated uint32 focused_client_ids = 3; +} + message FailedToStartWebServerPayload { string error = 1; } @@ -284,6 +296,7 @@ message SessionManifest { bool web_clients_allowed = 8; uint32 web_client_count = 9; repeated ClientTabHistory tab_history = 10; + repeated ClientPaneHistory pane_history = 11; } message ClientTabHistory { @@ -291,6 +304,11 @@ message ClientTabHistory { repeated uint32 tab_history = 2; } +message ClientPaneHistory { + uint32 client_id = 1; + repeated PaneId pane_history = 2; +} + message PluginInfo { uint32 plugin_id = 1; string plugin_url = 2; @@ -418,3 +436,16 @@ message SelectedText { action.Position start = 1; action.Position end = 2; } + +message UserActionPayload { + action.Action action = 1; + uint32 client_id = 2; + optional uint32 terminal_id = 3; + optional uint32 cli_client_id = 4; +} + +message ActionCompletePayload { + action.Action action = 1; + optional PaneId pane_id = 2; + repeated ContextItem context = 3; +} diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index 02ef1842..4e430b0b 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -2,20 +2,24 @@ pub use super::generated_api::api::{ action::{Action as ProtobufAction, Position as ProtobufPosition}, event::{ event::Payload as ProtobufEventPayload, pane_scrollback_response, - ClientInfo as ProtobufClientInfo, ClientTabHistory as ProtobufClientTabHistory, - CopyDestination as ProtobufCopyDestination, Event as ProtobufEvent, - EventNameList as ProtobufEventNameList, EventType as ProtobufEventType, - FileMetadata as ProtobufFileMetadata, InputModeKeybinds as ProtobufInputModeKeybinds, - KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo, - ModeUpdatePayload as ProtobufModeUpdatePayload, PaneContents as ProtobufPaneContents, - PaneContentsEntry as ProtobufPaneContentsEntry, PaneId as ProtobufPaneId, - PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest, + ActionCompletePayload as ProtobufActionCompletePayload, ClientInfo as ProtobufClientInfo, + ClientPaneHistory as ProtobufClientPaneHistory, + ClientTabHistory as ProtobufClientTabHistory, ContextItem as ProtobufContextItem, + CopyDestination as ProtobufCopyDestination, CwdChangedPayload as ProtobufCwdChangedPayload, + Event as ProtobufEvent, EventNameList as ProtobufEventNameList, + EventType as ProtobufEventType, FileMetadata as ProtobufFileMetadata, + InputModeKeybinds as ProtobufInputModeKeybinds, KeyBind as ProtobufKeyBind, + LayoutInfo as ProtobufLayoutInfo, ModeUpdatePayload as ProtobufModeUpdatePayload, + PaneContents as ProtobufPaneContents, PaneContentsEntry as ProtobufPaneContentsEntry, + PaneId as ProtobufPaneId, PaneInfo as ProtobufPaneInfo, + PaneManifest as ProtobufPaneManifest, PaneRenderReportPayload as ProtobufPaneRenderReportPayload, PaneScrollbackResponse as ProtobufPaneScrollbackResponse, PaneType as ProtobufPaneType, PluginInfo as ProtobufPluginInfo, ResurrectableSession as ProtobufResurrectableSession, SelectedText as ProtobufSelectedText, SessionManifest as ProtobufSessionManifest, - TabInfo as ProtobufTabInfo, WebServerStatusPayload as ProtobufWebServerStatusPayload, - WebSharing as ProtobufWebSharing, *, + TabInfo as ProtobufTabInfo, UserActionPayload as ProtobufUserActionPayload, + WebServerStatusPayload as ProtobufWebServerStatusPayload, WebSharing as ProtobufWebSharing, + *, }, input_mode::InputMode as ProtobufInputMode, key::Key as ProtobufKey, @@ -23,8 +27,8 @@ pub use super::generated_api::api::{ }; #[allow(hidden_glob_reexports)] use crate::data::{ - ClientInfo, CopyDestination, Event, EventType, FileMetadata, InputMode, KeyWithModifier, - LayoutInfo, ModeInfo, Mouse, PaneContents, PaneId, PaneInfo, PaneManifest, + ClientId, ClientInfo, CopyDestination, Event, EventType, FileMetadata, InputMode, + KeyWithModifier, LayoutInfo, ModeInfo, Mouse, PaneContents, PaneId, PaneInfo, PaneManifest, PaneScrollbackResponse, PermissionStatus, PluginCapabilities, PluginInfo, SelectedText, SessionInfo, Style, TabInfo, WebServerStatus, WebSharing, }; @@ -397,6 +401,63 @@ impl TryFrom<ProtobufEvent> for Event { }, _ => Err("Malformed payload for the PaneRenderReport Event"), }, + Some(ProtobufEventType::UserAction) => match protobuf_event.payload { + Some(ProtobufEventPayload::UserActionPayload(protobuf_payload)) => { + let action: Action = protobuf_payload + .action + .ok_or("Missing action in UserAction payload")? + .try_into() + .map_err(|_| "Failed to convert Action in UserAction payload")?; + let client_id = protobuf_payload.client_id as u16; + let terminal_id = protobuf_payload.terminal_id; + let cli_client_id = protobuf_payload.cli_client_id.map(|id| id as u16); + Ok(Event::UserAction( + action, + client_id, + terminal_id, + cli_client_id, + )) + }, + _ => Err("Malformed payload for the UserAction Event"), + }, + Some(ProtobufEventType::ActionComplete) => match protobuf_event.payload { + Some(ProtobufEventPayload::ActionCompletePayload(protobuf_payload)) => { + let action: Action = protobuf_payload + .action + .ok_or("Missing action in ActionComplete payload")? + .try_into() + .map_err(|_| "Failed to convert Action in ActionComplete payload")?; + let pane_id = protobuf_payload + .pane_id + .map(|id| id.try_into()) + .transpose() + .map_err(|_| "Failed to convert PaneId in ActionComplete payload")?; + let context: BTreeMap<String, String> = protobuf_payload + .context + .into_iter() + .map(|item| (item.name, item.value)) + .collect(); + Ok(Event::ActionComplete(action, pane_id, context)) + }, + _ => Err("Malformed payload for the ActionComplete Event"), + }, + Some(ProtobufEventType::CwdChanged) => match protobuf_event.payload { + Some(ProtobufEventPayload::CwdChangedPayload(protobuf_payload)) => { + let pane_id: PaneId = protobuf_payload + .pane_id + .ok_or("Missing pane_id in CwdChanged payload")? + .try_into() + .map_err(|_| "Failed to convert PaneId in CwdChanged payload")?; + let new_cwd = PathBuf::from(protobuf_payload.new_cwd); + let focused_client_ids: Vec<ClientId> = protobuf_payload + .focused_client_ids + .into_iter() + .map(|id| id as u16) + .collect(); + Ok(Event::CwdChanged(pane_id, new_cwd, focused_client_ids)) + }, + _ => Err("Malformed payload for the CwdChanged Event"), + }, None => Err("Unknown Protobuf Event"), } } @@ -797,6 +858,58 @@ impl TryFrom<Event> for ProtobufEvent { pane_contents_map.try_into()?, )), }), + Event::UserAction(action, client_id, terminal_id, cli_client_id) => { + let protobuf_action: ProtobufAction = action + .try_into() + .map_err(|_| "Failed to convert Action to protobuf")?; + let protobuf_payload = ProtobufUserActionPayload { + action: Some(protobuf_action), + client_id: client_id as u32, + terminal_id, + cli_client_id: cli_client_id.map(|id| id as u32), + }; + Ok(ProtobufEvent { + name: ProtobufEventType::UserAction as i32, + payload: Some(event::Payload::UserActionPayload(protobuf_payload)), + }) + }, + Event::ActionComplete(action, pane_id, context) => { + let protobuf_action = action.try_into()?; + let protobuf_pane_id = pane_id.map(|id| id.try_into()).transpose()?; + let context_items: Vec<ProtobufContextItem> = context + .into_iter() + .map(|(name, value)| ProtobufContextItem { name, value }) + .collect(); + let action_complete_payload = ProtobufActionCompletePayload { + action: Some(protobuf_action), + pane_id: protobuf_pane_id, + context: context_items, + }; + Ok(ProtobufEvent { + name: ProtobufEventType::ActionComplete as i32, + payload: Some(event::Payload::ActionCompletePayload( + action_complete_payload, + )), + }) + }, + Event::CwdChanged(pane_id, new_cwd, focused_client_ids) => { + let protobuf_pane_id: ProtobufPaneId = pane_id.try_into()?; + let new_cwd_string = new_cwd + .to_str() + .ok_or("Failed to convert PathBuf to string")? + .to_string(); + let focused_client_ids_u32: Vec<u32> = + focused_client_ids.into_iter().map(|id| id as u32).collect(); + let cwd_changed_payload = ProtobufCwdChangedPayload { + pane_id: Some(protobuf_pane_id), + new_cwd: new_cwd_string, + focused_client_ids: focused_client_ids_u32, + }; + Ok(ProtobufEvent { + name: ProtobufEventType::CwdChanged as i32, + payload: Some(event::Payload::CwdChangedPayload(cwd_changed_payload)), + }) + }, } } } @@ -842,6 +955,11 @@ impl TryFrom<SessionInfo> for ProtobufSessionManifest { .into_iter() .map(|t| ProtobufClientTabHistory::from(t)) .collect(), + pane_history: session_info + .pane_history + .into_iter() + .map(|p| ProtobufClientPaneHistory::from(p)) + .collect(), }) } } @@ -854,6 +972,18 @@ impl From<(u16, Vec<usize>)> for ProtobufClientTabHistory { } } } + +impl From<(u16, Vec<PaneId>)> for ProtobufClientPaneHistory { + fn from((client_id, pane_history): (u16, Vec<PaneId>)) -> ProtobufClientPaneHistory { + ProtobufClientPaneHistory { + client_id: client_id as u32, + pane_history: pane_history + .into_iter() + .filter_map(|p| p.try_into().ok()) + .collect(), + } + } +} impl From<(u32, PluginInfo)> for ProtobufPluginInfo { fn from((plugin_id, plugin_info): (u32, PluginInfo)) -> ProtobufPluginInfo { ProtobufPluginInfo { @@ -910,6 +1040,16 @@ impl TryFrom<ProtobufSessionManifest> for SessionInfo { .collect(); tab_history.insert(client_id as u16, tab_history_for_client); } + let mut pane_history = BTreeMap::new(); + for client_pane_history in protobuf_session_manifest.pane_history.into_iter() { + let client_id = client_pane_history.client_id; + let pane_history_for_client = client_pane_history + .pane_history + .into_iter() + .filter_map(|p| p.try_into().ok()) + .collect(); + pane_history.insert(client_id as u16, pane_history_for_client); + } Ok(SessionInfo { name: protobuf_session_manifest.name, tabs: protobuf_session_manifest @@ -929,6 +1069,7 @@ impl TryFrom<ProtobufSessionManifest> for SessionInfo { web_clients_allowed: protobuf_session_manifest.web_clients_allowed, web_client_count: protobuf_session_manifest.web_client_count as usize, tab_history, + pane_history, }) } } @@ -1476,6 +1617,9 @@ impl TryFrom<ProtobufEventType> for EventType { ProtobufEventType::FailedToStartWebServer => EventType::FailedToStartWebServer, ProtobufEventType::InterceptedKeyPress => EventType::InterceptedKeyPress, ProtobufEventType::PaneRenderReport => EventType::PaneRenderReport, + ProtobufEventType::UserAction => EventType::UserAction, + ProtobufEventType::ActionComplete => EventType::ActionComplete, + ProtobufEventType::CwdChanged => EventType::CwdChanged, }) } } @@ -1520,6 +1664,9 @@ impl TryFrom<EventType> for ProtobufEventType { EventType::FailedToStartWebServer => ProtobufEventType::FailedToStartWebServer, EventType::InterceptedKeyPress => ProtobufEventType::InterceptedKeyPress, EventType::PaneRenderReport => ProtobufEventType::PaneRenderReport, + EventType::UserAction => ProtobufEventType::UserAction, + EventType::ActionComplete => ProtobufEventType::ActionComplete, + EventType::CwdChanged => ProtobufEventType::CwdChanged, }) } } @@ -2129,6 +2276,7 @@ fn serialize_session_update_event_with_non_default_values() { web_clients_allowed: false, web_client_count: 1, tab_history, + pane_history: Default::default(), }; let session_info_2 = SessionInfo { name: "session 2".to_owned(), @@ -2147,6 +2295,7 @@ fn serialize_session_update_event_with_non_default_values() { web_clients_allowed: false, web_client_count: 0, tab_history: Default::default(), + pane_history: Default::default(), }; let session_infos = vec![session_info_1, session_info_2]; let resurrectable_sessions = vec![]; diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 7ffe2fc1..f662c93b 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -160,6 +160,12 @@ enum CommandName { ClearKeyPressesIntercepts = 144; ReplacePaneWithExistingPane = 155; GetPaneScrollback = 163; + RunAction = 164; + ShowCursor = 169; + CopyToClipboard = 170; + SendSigintToPaneId = 171; + SendSigkillToPaneId = 172; + GetPanePid = 173; } message PluginCommand { @@ -268,6 +274,12 @@ message PluginCommand { ReplacePaneWithExistingPanePayload replace_pane_with_existing_pane_payload = 111; NewTabPayload new_tab_payload = 112; GetPaneScrollbackPayload get_pane_scrollback_payload = 113; + RunActionPayload run_action_payload = 114; + ShowCursorPayload show_cursor_payload = 119; + CopyToClipboardPayload copy_to_clipboard_payload = 120; + PaneId send_sigint_to_pane_id_payload = 121; + PaneId send_sigkill_to_pane_id_payload = 122; + GetPanePidPayload get_pane_pid_payload = 123; } } @@ -276,9 +288,15 @@ message NewTabPayload { optional string cwd = 2; } +message RunActionPayload { + action.Action action = 1; + repeated ContextItem context = 2; +} + message ReplacePaneWithExistingPanePayload { PaneId pane_id_to_replace = 1; PaneId existing_pane_id = 2; + bool suppress_replaced_pane = 3; } message RenameWebLoginTokenPayload { @@ -516,6 +534,7 @@ message HidePaneWithIdPayload { message ShowPaneWithIdPayload { PaneId pane_id = 1; bool should_float_if_hidden = 2; + bool should_focus_pane = 3; } message NewTabsWithLayoutInfoPayload { @@ -710,3 +729,27 @@ message GetPaneScrollbackPayload { PaneId pane_id = 1; bool get_full_scrollback = 2; } + +message ShowCursorPayload { + optional CursorPosition position = 1; +} + +message CursorPosition { + uint32 x = 1; + uint32 y = 2; +} + +message CopyToClipboardPayload { + string text = 1; +} + +message GetPanePidPayload { + PaneId pane_id = 1; +} + +message GetPanePidResponse { + oneof result { + int32 pid = 1; + string error = 2; + } +} diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index c1ba877e..771bb9ec 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -1,17 +1,19 @@ pub use super::generated_api::api::{ - action::{PaneIdAndShouldFloat, SwitchToModePayload}, + action::{Action as ProtobufAction, PaneIdAndShouldFloat, SwitchToModePayload}, event::{EventNameList as ProtobufEventNameList, Header}, input_mode::InputMode as ProtobufInputMode, plugin_command::{ - plugin_command::Payload, BreakPanesToNewTabPayload, BreakPanesToTabWithIndexPayload, - ChangeFloatingPanesCoordinatesPayload, ChangeHostFolderPayload, - ClearScreenForPaneIdPayload, CliPipeOutputPayload, CloseMultiplePanesPayload, - CloseTabWithIndexPayload, CommandName, ContextItem, - CreateTokenResponse as ProtobufCreateTokenResponse, CreateTokenResponse, - EditScrollbackForPaneWithIdPayload, EmbedMultiplePanesPayload, EnvVariable, ExecCmdPayload, + get_pane_pid_response, plugin_command::Payload, BreakPanesToNewTabPayload, + BreakPanesToTabWithIndexPayload, ChangeFloatingPanesCoordinatesPayload, + ChangeHostFolderPayload, ClearScreenForPaneIdPayload, CliPipeOutputPayload, + CloseMultiplePanesPayload, CloseTabWithIndexPayload, CommandName, ContextItem, + CopyToClipboardPayload, CreateTokenResponse as ProtobufCreateTokenResponse, + CreateTokenResponse, CursorPosition, EditScrollbackForPaneWithIdPayload, + EmbedMultiplePanesPayload, EnvVariable, ExecCmdPayload, FixedOrPercent as ProtobufFixedOrPercent, FixedOrPercentValue as ProtobufFixedOrPercentValue, FloatMultiplePanesPayload, FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, GenerateWebLoginTokenPayload, + GetPanePidPayload, GetPanePidResponse as ProtobufGetPanePidResponse, GetPaneScrollbackPayload, GroupAndUngroupPanesPayload, HidePaneWithIdPayload, HighlightAndUnhighlightPanesPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload, ListTokensResponse, LoadNewPluginPayload, @@ -29,20 +31,21 @@ pub use super::generated_api::api::{ RenameWebTokenResponse, ReplacePaneWithExistingPanePayload, RequestPluginPermissionPayload, RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload, RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload, - RunCommandPayload, ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, - ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SetFloatingPanePinnedPayload, - SetSelfMouseSelectionSupportPayload, SetTimeoutPayload, ShowPaneWithIdPayload, - StackPanesPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, - TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, UnsubscribePayload, - WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload, + RunActionPayload, RunCommandPayload, ScrollDownInPaneIdPayload, + ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, + SetFloatingPanePinnedPayload, SetSelfMouseSelectionSupportPayload, SetTimeoutPayload, + ShowCursorPayload, ShowPaneWithIdPayload, StackPanesPayload, SubscribePayload, + SwitchSessionPayload, SwitchTabToPayload, TogglePaneEmbedOrEjectForPaneIdPayload, + TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload, + WriteCharsToPaneIdPayload, WriteToPaneIdPayload, }, plugin_permission::PermissionType as ProtobufPermissionType, resize::ResizeAction as ProtobufResizeAction, }; use crate::data::{ - ConnectToSession, FloatingPaneCoordinates, HttpVerb, InputMode, KeyWithModifier, - MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand, + ConnectToSession, FloatingPaneCoordinates, GetPanePidResponse, HttpVerb, InputMode, + KeyWithModifier, MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand, }; use crate::input::actions::Action; use crate::input::layout::SplitSize; @@ -200,6 +203,30 @@ impl TryFrom<PaneId> for ProtobufPaneId { } } +impl TryFrom<ProtobufGetPanePidResponse> for GetPanePidResponse { + type Error = &'static str; + fn try_from(protobuf_response: ProtobufGetPanePidResponse) -> Result<Self, &'static str> { + match protobuf_response.result { + Some(get_pane_pid_response::Result::Pid(pid)) => Ok(GetPanePidResponse::Ok(pid)), + Some(get_pane_pid_response::Result::Error(error)) => Ok(GetPanePidResponse::Err(error)), + None => Err("Empty GetPanePidResponse"), + } + } +} + +impl From<GetPanePidResponse> for ProtobufGetPanePidResponse { + fn from(response: GetPanePidResponse) -> Self { + match response { + GetPanePidResponse::Ok(pid) => ProtobufGetPanePidResponse { + result: Some(get_pane_pid_response::Result::Pid(pid)), + }, + GetPanePidResponse::Err(error) => ProtobufGetPanePidResponse { + result: Some(get_pane_pid_response::Result::Error(error)), + }, + } + } +} + impl TryFrom<(InputMode, KeyWithModifier, Vec<Action>)> for KeyToRebind { type Error = &'static str; fn try_from( @@ -288,6 +315,14 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand { }, _ => Err("Mismatched payload for SetSelectable"), }, + Some(CommandName::ShowCursor) => match protobuf_plugin_command.payload { + Some(Payload::ShowCursorPayload(payload)) => { + let cursor_position = + payload.position.map(|pos| (pos.x as usize, pos.y as usize)); + Ok(PluginCommand::ShowCursor(cursor_position)) + }, + _ => Err("Mismatched payload for ShowCursor"), + }, Some(CommandName::GetPluginIds) => { if protobuf_plugin_command.payload.is_some() { Err("GetPluginIds should not have a payload") @@ -734,7 +769,12 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand { Some(Payload::FocusTerminalPanePayload(payload)) => { let pane_id = payload.pane_id as u32; let should_float = payload.should_float; - Ok(PluginCommand::FocusTerminalPane(pane_id, should_float)) + let should_be_in_place = payload.should_be_in_place; + Ok(PluginCommand::FocusTerminalPane( + pane_id, + should_float, + should_be_in_place, + )) }, _ => Err("Mismatched payload for ClosePluginPane"), }, @@ -742,7 +782,12 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand { Some(Payload::FocusPluginPanePayload(payload)) => { let pane_id = payload.pane_id as u32; let should_float = payload.should_float; - Ok(PluginCommand::FocusPluginPane(pane_id, should_float)) + let should_be_in_place = payload.should_be_in_place; + Ok(PluginCommand::FocusPluginPane( + pane_id, + should_float, + should_be_in_place, + )) }, _ => Err("Mismatched payload for ClosePluginPane"), }, @@ -1048,9 +1093,11 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand { .and_then(|p_id| PaneId::try_from(p_id).ok()) .ok_or("Failed to parse ShowPaneWithId command")?; let should_float_if_hidden = show_pane_with_id_payload.should_float_if_hidden; + let should_focus_pane = show_pane_with_id_payload.should_focus_pane; Ok(PluginCommand::ShowPaneWithId( pane_id, should_float_if_hidden, + should_focus_pane, )) }, _ => Err("Mismatched payload for ShowPaneWithId"), @@ -1145,6 +1192,29 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand { }, _ => Err("Mismatched payload for WriteCharsCharsToPaneId"), }, + Some(CommandName::SendSigintToPaneId) => match protobuf_plugin_command.payload { + Some(Payload::SendSigintToPaneIdPayload(pane_id)) => { + Ok(PluginCommand::SendSigintToPaneId(pane_id.try_into()?)) + }, + _ => Err("Mismatched payload for SendSigintToPaneId"), + }, + Some(CommandName::SendSigkillToPaneId) => match protobuf_plugin_command.payload { + Some(Payload::SendSigkillToPaneIdPayload(pane_id)) => { + Ok(PluginCommand::SendSigkillToPaneId(pane_id.try_into()?)) + }, + _ => Err("Mismatched payload for SendSigkillToPaneId"), + }, + Some(CommandName::GetPanePid) => match protobuf_plugin_command.payload { + Some(Payload::GetPanePidPayload(get_pane_pid_payload)) => { + match get_pane_pid_payload.pane_id { + Some(pane_id) => Ok(PluginCommand::GetPanePid { + pane_id: pane_id.try_into()?, + }), + _ => Err("Malformed get_pane_pid payload"), + } + }, + _ => Err("Mismatched payload for GetPanePid"), + }, Some(CommandName::MovePaneWithPaneId) => match protobuf_plugin_command.payload { Some(Payload::MovePaneWithPaneIdPayload(move_pane_with_pane_id_payload)) => { match move_pane_with_pane_id_payload.pane_id { @@ -1745,9 +1815,35 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand { .existing_pane_id .and_then(|p_id| PaneId::try_from(p_id).ok()) .ok_or("Failed to parse ReplacePaneWithExistingPanePayload")?, + replace_pane_with_other_pane_payload.suppress_replaced_pane, )), _ => Err("Mismatched payload for ReplacePaneWithExistingPane"), }, + Some(CommandName::RunAction) => match protobuf_plugin_command.payload { + Some(Payload::RunActionPayload(protobuf_payload)) => { + let action = Action::try_from( + protobuf_payload + .action + .ok_or("Missing action in RunAction payload")?, + ) + .map_err(|_| "Failed to convert protobuf action")?; + + let context: BTreeMap<String, String> = protobuf_payload + .context + .into_iter() + .map(|item| (item.name, item.value)) + .collect(); + + Ok(PluginCommand::RunAction(action, context)) + }, + _ => Err("Mismatched payload for RunAction"), + }, + Some(CommandName::CopyToClipboard) => match protobuf_plugin_command.payload { + Some(Payload::CopyToClipboardPayload(payload)) => { + Ok(PluginCommand::CopyToClipboard(payload.text)) + }, + _ => Err("Mismatched payload for CopyToClipboard"), + }, None => Err("Unrecognized plugin command"), } } @@ -1779,6 +1875,16 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { name: CommandName::SetSelectable as i32, payload: Some(Payload::SetSelectablePayload(should_be_selectable)), }), + PluginCommand::ShowCursor(cursor_position) => { + let position = cursor_position.map(|(x, y)| CursorPosition { + x: x as u32, + y: y as u32, + }); + Ok(ProtobufPluginCommand { + name: CommandName::ShowCursor as i32, + payload: Some(Payload::ShowCursorPayload(ShowCursorPayload { position })), + }) + }, PluginCommand::GetPluginIds => Ok(ProtobufPluginCommand { name: CommandName::GetPluginIds as i32, payload: None, @@ -2077,24 +2183,30 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { name: CommandName::ClosePluginPane as i32, payload: Some(Payload::ClosePluginPanePayload(pane_id)), }), - PluginCommand::FocusTerminalPane(pane_id, should_float_if_hidden) => { - Ok(ProtobufPluginCommand { - name: CommandName::FocusTerminalPane as i32, - payload: Some(Payload::FocusTerminalPanePayload(PaneIdAndShouldFloat { - pane_id: pane_id, - should_float: should_float_if_hidden, - })), - }) - }, - PluginCommand::FocusPluginPane(pane_id, should_float_if_hidden) => { - Ok(ProtobufPluginCommand { - name: CommandName::FocusPluginPane as i32, - payload: Some(Payload::FocusPluginPanePayload(PaneIdAndShouldFloat { - pane_id: pane_id, - should_float: should_float_if_hidden, - })), - }) - }, + PluginCommand::FocusTerminalPane( + pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ) => Ok(ProtobufPluginCommand { + name: CommandName::FocusTerminalPane as i32, + payload: Some(Payload::FocusTerminalPanePayload(PaneIdAndShouldFloat { + pane_id: pane_id, + should_float: should_float_if_hidden, + should_be_in_place: should_be_in_place_if_hidden, + })), + }), + PluginCommand::FocusPluginPane( + pane_id, + should_float_if_hidden, + should_be_in_place_if_hidden, + ) => Ok(ProtobufPluginCommand { + name: CommandName::FocusPluginPane as i32, + payload: Some(Payload::FocusPluginPanePayload(PaneIdAndShouldFloat { + pane_id: pane_id, + should_float: should_float_if_hidden, + should_be_in_place: should_be_in_place_if_hidden, + })), + }), PluginCommand::RenameTerminalPane(pane_id, new_name) => Ok(ProtobufPluginCommand { name: CommandName::RenameTerminalPane as i32, payload: Some(Payload::RenameTerminalPanePayload(IdAndNewName { @@ -2335,15 +2447,18 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { pane_id: ProtobufPaneId::try_from(pane_id_to_hide).ok(), })), }), - PluginCommand::ShowPaneWithId(pane_id_to_show, should_float_if_hidden) => { - Ok(ProtobufPluginCommand { - name: CommandName::ShowPaneWithId as i32, - payload: Some(Payload::ShowPaneWithIdPayload(ShowPaneWithIdPayload { - pane_id: ProtobufPaneId::try_from(pane_id_to_show).ok(), - should_float_if_hidden, - })), - }) - }, + PluginCommand::ShowPaneWithId( + pane_id_to_show, + should_float_if_hidden, + should_focus_pane, + ) => Ok(ProtobufPluginCommand { + name: CommandName::ShowPaneWithId as i32, + payload: Some(Payload::ShowPaneWithIdPayload(ShowPaneWithIdPayload { + pane_id: ProtobufPaneId::try_from(pane_id_to_show).ok(), + should_float_if_hidden, + should_focus_pane, + })), + }), PluginCommand::OpenCommandPaneBackground(command_to_run, context) => { let context: Vec<_> = context .into_iter() @@ -2415,6 +2530,20 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { )), }) }, + PluginCommand::SendSigintToPaneId(pane_id) => Ok(ProtobufPluginCommand { + name: CommandName::SendSigintToPaneId as i32, + payload: Some(Payload::SendSigintToPaneIdPayload(pane_id.try_into()?)), + }), + PluginCommand::SendSigkillToPaneId(pane_id) => Ok(ProtobufPluginCommand { + name: CommandName::SendSigkillToPaneId as i32, + payload: Some(Payload::SendSigkillToPaneIdPayload(pane_id.try_into()?)), + }), + PluginCommand::GetPanePid { pane_id } => Ok(ProtobufPluginCommand { + name: CommandName::GetPanePid as i32, + payload: Some(Payload::GetPanePidPayload(GetPanePidPayload { + pane_id: Some(pane_id.try_into()?), + })), + }), PluginCommand::MovePaneWithPaneId(pane_id) => Ok(ProtobufPluginCommand { name: CommandName::MovePaneWithPaneId as i32, payload: Some(Payload::MovePaneWithPaneIdPayload( @@ -2901,17 +3030,43 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { name: CommandName::ClearKeyPressesIntercepts as i32, payload: None, }), - PluginCommand::ReplacePaneWithExistingPane(pane_id_to_replace, existing_pane_id) => { + PluginCommand::ReplacePaneWithExistingPane( + pane_id_to_replace, + existing_pane_id, + suppress_replaced_pane, + ) => Ok(ProtobufPluginCommand { + name: CommandName::ReplacePaneWithExistingPane as i32, + payload: Some(Payload::ReplacePaneWithExistingPanePayload( + ReplacePaneWithExistingPanePayload { + pane_id_to_replace: ProtobufPaneId::try_from(pane_id_to_replace).ok(), + existing_pane_id: ProtobufPaneId::try_from(existing_pane_id).ok(), + suppress_replaced_pane, + }, + )), + }), + PluginCommand::RunAction(action, context) => { + let protobuf_action = ProtobufAction::try_from(action) + .map_err(|_| "Failed to convert action to protobuf")?; + + let context_items: Vec<ContextItem> = context + .into_iter() + .map(|(name, value)| ContextItem { name, value }) + .collect(); + Ok(ProtobufPluginCommand { - name: CommandName::ReplacePaneWithExistingPane as i32, - payload: Some(Payload::ReplacePaneWithExistingPanePayload( - ReplacePaneWithExistingPanePayload { - pane_id_to_replace: ProtobufPaneId::try_from(pane_id_to_replace).ok(), - existing_pane_id: ProtobufPaneId::try_from(existing_pane_id).ok(), - }, - )), + name: CommandName::RunAction as i32, + payload: Some(Payload::RunActionPayload(RunActionPayload { + action: Some(protobuf_action), + context: context_items, + })), }) }, + PluginCommand::CopyToClipboard(text) => Ok(ProtobufPluginCommand { + name: CommandName::CopyToClipboard as i32, + payload: Some(Payload::CopyToClipboardPayload(CopyToClipboardPayload { + text, + })), + }), } } } diff --git a/zellij-utils/src/plugin_api/plugin_permission.proto b/zellij-utils/src/plugin_api/plugin_permission.proto index 85126830..779f6b32 100644 --- a/zellij-utils/src/plugin_api/plugin_permission.proto +++ b/zellij-utils/src/plugin_api/plugin_permission.proto @@ -17,4 +17,6 @@ enum PermissionType { StartWebServer = 11; InterceptInput = 12; ReadPaneContents = 13; + RunActionsAsUser = 14; + WriteToClipboard = 15; } diff --git a/zellij-utils/src/plugin_api/plugin_permission.rs b/zellij-utils/src/plugin_api/plugin_permission.rs index 307d3576..37782a08 100644 --- a/zellij-utils/src/plugin_api/plugin_permission.rs +++ b/zellij-utils/src/plugin_api/plugin_permission.rs @@ -29,6 +29,8 @@ impl TryFrom<ProtobufPermissionType> for PermissionType { ProtobufPermissionType::StartWebServer => Ok(PermissionType::StartWebServer), ProtobufPermissionType::InterceptInput => Ok(PermissionType::InterceptInput), ProtobufPermissionType::ReadPaneContents => Ok(PermissionType::ReadPaneContents), + ProtobufPermissionType::RunActionsAsUser => Ok(PermissionType::RunActionsAsUser), + ProtobufPermissionType::WriteToClipboard => Ok(PermissionType::WriteToClipboard), } } } @@ -59,6 +61,8 @@ impl TryFrom<PermissionType> for ProtobufPermissionType { PermissionType::StartWebServer => Ok(ProtobufPermissionType::StartWebServer), PermissionType::InterceptInput => Ok(ProtobufPermissionType::InterceptInput), PermissionType::ReadPaneContents => Ok(ProtobufPermissionType::ReadPaneContents), + PermissionType::RunActionsAsUser => Ok(ProtobufPermissionType::RunActionsAsUser), + PermissionType::WriteToClipboard => Ok(ProtobufPermissionType::WriteToClipboard), } } } diff --git a/zellij-utils/src/position.rs b/zellij-utils/src/position.rs index 3566625b..95987c27 100644 --- a/zellij-utils/src/position.rs +++ b/zellij-utils/src/position.rs @@ -37,6 +37,12 @@ impl Position { } } +impl Default for Position { + fn default() -> Self { + Position::new(0, 0) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd)] pub struct Line(pub isize); #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, PartialOrd)] 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 e01b7410..3e60c317 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 @@ -1348,6 +1348,7 @@ Config { NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }, SwitchToMode { input_mode: Normal, @@ -1853,6 +1854,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -4446,6 +4449,36 @@ Config { bare_key: Char( 'q', ), + key_modifiers: {}, + }: [ + LaunchPlugin { + plugin: RunPlugin( + RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, + ), + should_float: true, + should_open_in_place: false, + skip_cache: false, + cwd: None, + }, + SwitchToMode { + input_mode: Normal, + }, + ], + KeyWithModifier { + bare_key: Char( + 'q', + ), key_modifiers: { Ctrl, }, @@ -5584,6 +5617,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -6036,6 +6071,18 @@ Config { ), initial_cwd: None, }, + "sequence": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( 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 9c280d9c..e0fa22f9 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 @@ -1348,6 +1348,7 @@ Config { NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }, SwitchToMode { input_mode: Normal, @@ -1853,6 +1854,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -4446,6 +4449,36 @@ Config { bare_key: Char( 'q', ), + key_modifiers: {}, + }: [ + LaunchPlugin { + plugin: RunPlugin( + RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, + ), + should_float: true, + should_open_in_place: false, + skip_cache: false, + cwd: None, + }, + SwitchToMode { + input_mode: Normal, + }, + ], + KeyWithModifier { + bare_key: Char( + 'q', + ), key_modifiers: { Ctrl, }, @@ -5584,6 +5617,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -6036,6 +6071,18 @@ Config { ), initial_cwd: None, }, + "sequence": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( 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 773ae7e4..1d245a3e 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 @@ -195,6 +195,18 @@ Config { ), initial_cwd: None, }, + "sequence": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( 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 0406faa8..e9935ef3 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 @@ -1348,6 +1348,7 @@ Config { NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }, SwitchToMode { input_mode: Normal, @@ -1853,6 +1854,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -4446,6 +4449,36 @@ Config { bare_key: Char( 'q', ), + key_modifiers: {}, + }: [ + LaunchPlugin { + plugin: RunPlugin( + RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, + ), + should_float: true, + should_open_in_place: false, + skip_cache: false, + cwd: None, + }, + SwitchToMode { + input_mode: Normal, + }, + ], + KeyWithModifier { + bare_key: Char( + 'q', + ), key_modifiers: { Ctrl, }, @@ -5584,6 +5617,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -7807,6 +7842,18 @@ Config { ), initial_cwd: None, }, + "sequence": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( 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 ca2b7bae..368b5d88 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 @@ -1348,6 +1348,7 @@ Config { NewStackedPane { command: None, pane_name: None, + near_current_pane: false, }, SwitchToMode { input_mode: Normal, @@ -1853,6 +1854,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -4446,6 +4449,36 @@ Config { bare_key: Char( 'q', ), + key_modifiers: {}, + }: [ + LaunchPlugin { + plugin: RunPlugin( + RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, + ), + should_float: true, + should_open_in_place: false, + skip_cache: false, + cwd: None, + }, + SwitchToMode { + input_mode: Normal, + }, + ], + KeyWithModifier { + bare_key: Char( + 'q', + ), key_modifiers: { Ctrl, }, @@ -5584,6 +5617,8 @@ Config { tab_name: None, should_change_focus_to_new_tab: true, cwd: None, + initial_panes: None, + first_pane_unblock_condition: None, }, SwitchToMode { input_mode: Normal, @@ -6036,6 +6071,18 @@ Config { ), initial_cwd: None, }, + "sequence": RunPlugin { + _allow_exec_host_cmd: false, + location: Zellij( + PluginTag( + "sequence", + ), + ), + configuration: PluginUserConfiguration( + {}, + ), + initial_cwd: None, + }, "session-manager": RunPlugin { _allow_exec_host_cmd: false, location: Zellij( |
