aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-utils/src
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-01-13 11:27:07 +0100
committerGitHub <noreply@github.com>2026-01-13 11:27:07 +0100
commit4a27ceea0d99b036ca607e05714cef15689d7c26 (patch)
tree22c0148718a82082bdb196c5e867697f514cc541 /zellij-utils/src
parentdocs(CONTRIBUTING): Describe rust toolchain versioning policy (#4585) (diff)
downloadsl0ppij-4a27ceea0d99b036ca607e05714cef15689d7c26.tar.gz
sl0ppij-4a27ceea0d99b036ca607e05714cef15689d7c26.tar.xz
sl0ppij-4a27ceea0d99b036ca607e05714cef15689d7c26.zip
Layout manager (#4601)
* ad plugin override_layout command * watch available layouts and report to plugin thread * Event::AvailableLayoutInfo * save layout command * refactor save_layout * fix api and permissions * delete_layout API command * edit layout in place API * edit layout response through api * propagate layout parsing errors * generate random name plugin api * dump stringified layout API * synchronicity dump_session_layout and include LayoutMetadata in sync response * parse_layout plugin api * fix: default to default layout dir in plugin api * rename_layout plugin command API * initial implementation of multi-tab override * apply to multiple tabs * allow applying to only the active tab * add tab/pane names to layout parsing metadata * indicate is_builtin_plugin in PaneMetadata * get_layout_dir plugin API command * consolidate (and fix) is_builtin_plugin logic * fix bad ret * allow dumping the layout for just a specific tab in the session * get focused pane and tab sync api * color last substring variants * add plugin * update assets * fix tests * add e2e tests for new plugin apis * update ui with new shortcut * rustfmt * docs(changelog): add PR
Diffstat (limited to 'zellij-utils/src')
-rw-r--r--zellij-utils/src/cli.rs5
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto39
-rw-r--r--zellij-utils/src/consts.rs1
-rw-r--r--zellij-utils/src/data.rs346
-rw-r--r--zellij-utils/src/errors.rs3
-rw-r--r--zellij-utils/src/input/actions.rs60
-rw-r--r--zellij-utils/src/input/config.rs97
-rw-r--r--zellij-utils/src/input/layout.rs98
-rw-r--r--zellij-utils/src/input/plugins.rs1
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs233
-rw-r--r--zellij-utils/src/ipc/tests/roundtrip_tests.rs20
-rw-r--r--zellij-utils/src/kdl/mod.rs51
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string.snap7
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap7
-rw-r--r--zellij-utils/src/plugin_api/action.proto20
-rw-r--r--zellij-utils/src/plugin_api/action.rs181
-rw-r--r--zellij-utils/src/plugin_api/event.proto55
-rw-r--r--zellij-utils/src/plugin_api/event.rs304
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto137
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs385
-rw-r--r--zellij-utils/src/sessions.rs6
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap30
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap30
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap30
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap30
25 files changed, 1890 insertions, 286 deletions
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs
index 1d196df9..d5070f2f 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -950,6 +950,11 @@ pub enum CliAction {
/// Retain existing plugin panes that do not fit with the layout default: false)
#[clap(long, value_parser, takes_value(false), default_value("false"))]
retain_existing_plugin_panes: bool,
+
+ /// Only apply the layout to the active tab (uses just the first layout tab if it has
+ /// multiple)
+ #[clap(long, value_parser, takes_value(false), default_value("false"))]
+ apply_only_to_active_tab: bool,
},
/// Query all tab names
QueryTabNames,
diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto
index bbaeadad..3435718f 100644
--- a/zellij-utils/src/client_server_contract/common_types.proto
+++ b/zellij-utils/src/client_server_contract/common_types.proto
@@ -272,14 +272,20 @@ message DenyAction {}
message ToggleMouseModeAction {}
message PreviousSwapLayoutAction {}
message NextSwapLayoutAction {}
+message TabLayoutInfo {
+ uint32 tab_index = 1;
+ optional string tab_name = 2;
+ TiledPaneLayout tiled_layout = 3;
+ repeated FloatingPaneLayout floating_layouts = 4;
+ repeated SwapTiledLayout swap_tiled_layouts = 5;
+ repeated SwapFloatingLayout swap_floating_layouts = 6;
+}
+
message OverrideLayoutAction {
- 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 retain_existing_terminal_panes = 6;
- bool retain_existing_plugin_panes = 7;
+ repeated TabLayoutInfo tabs = 1;
+ bool retain_existing_terminal_panes = 2;
+ bool retain_existing_plugin_panes = 3;
+ bool apply_only_to_active_tab = 4;
}
message QueryTabNamesAction {}
message BreakPaneAction {}
@@ -635,6 +641,25 @@ message LayoutInfo {
string url = 3;
string stringified = 4;
}
+ optional LayoutMetadata layout_metadata = 5;
+}
+
+message LayoutMetadata {
+ repeated TabMetadata tabs = 1;
+ string creation_time = 2;
+ string update_time = 3;
+
+}
+
+message TabMetadata {
+ repeated PaneMetadata pane_metadata = 1;
+ optional string name = 2;
+}
+
+message PaneMetadata {
+ optional string name = 1;
+ bool is_plugin = 2;
+ bool is_builtin_plugin = 3;
}
message ConnectToSession {
diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs
index e5b93e1a..3faab0c9 100644
--- a/zellij-utils/src/consts.rs
+++ b/zellij-utils/src/consts.rs
@@ -139,6 +139,7 @@ mod not_wasm {
add_plugin!(assets, "share.wasm");
add_plugin!(assets, "multiple-select.wasm");
add_plugin!(assets, "sequence.wasm");
+ add_plugin!(assets, "layout-manager.wasm");
assets
};
}
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index 4a29375a..177d8c7b 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -1,8 +1,10 @@
use crate::home::default_layout_dir;
use crate::input::actions::{Action, RunCommandAction};
-use crate::input::config::ConversionError;
+use crate::input::config::{ConversionError, KdlError};
use crate::input::keybinds::Keybinds;
-use crate::input::layout::{RunPlugin, RunPluginOrAlias, SplitSize};
+use crate::input::layout::{
+ Layout, Run, RunPlugin, RunPluginLocation, RunPluginOrAlias, SplitSize,
+};
use crate::pane_size::PaneGeom;
use crate::position::Position;
use crate::shared::{colors as default_colors, eightbit_to_rgb};
@@ -11,6 +13,7 @@ use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fmt;
use std::fs::Metadata;
+use std::hash::{Hash, Hasher};
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::str::{self, FromStr};
@@ -612,6 +615,17 @@ impl KeyWithModifier {
}
true
}
+ pub fn has_only_modifiers(&self, modifiers: &[KeyModifier]) -> bool {
+ for modifier in modifiers {
+ if !self.key_modifiers.contains(modifier) {
+ return false;
+ }
+ }
+ if self.key_modifiers.len() != modifiers.len() {
+ return false;
+ }
+ true
+ }
}
#[derive(Eq, Clone, Copy, Debug, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)]
@@ -996,6 +1010,7 @@ pub enum Event {
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
+ AvailableLayoutInfo(Vec<LayoutInfo>, Vec<LayoutWithError>),
}
#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants, Display, Serialize, Deserialize)]
@@ -1717,16 +1732,266 @@ impl From<RunPlugin> for PluginInfo {
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub enum LayoutInfo {
BuiltIn(String),
- File(String),
+ File(String, LayoutMetadata),
Url(String),
Stringified(String),
}
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub struct LayoutWithError {
+ pub layout_name: String,
+ pub error: LayoutParsingError,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub enum LayoutParsingError {
+ KdlError {
+ kdl_error: KdlError,
+ file_name: String,
+ source_code: String,
+ },
+ SyntaxError,
+}
+
+impl AsRef<LayoutInfo> for LayoutInfo {
+ fn as_ref(&self) -> &LayoutInfo {
+ self
+ }
+}
+
+#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub struct LayoutMetadata {
+ pub tabs: Vec<TabMetadata>,
+ pub creation_time: String,
+ pub update_time: String,
+}
+
+impl From<&PathBuf> for LayoutMetadata {
+ fn from(path: &PathBuf) -> LayoutMetadata {
+ match Layout::stringified_from_path(path) {
+ Ok((path_str, stringified_layout, _swap_layouts)) => {
+ match Layout::from_kdl(&stringified_layout, Some(path_str), None, None) {
+ Ok(layout) => {
+ let layout_tabs = layout.tabs();
+ let tabs = if layout_tabs.is_empty() {
+ let (tiled_pane_layout, floating_pane_layout) = layout.new_tab();
+ vec![TabMetadata::from(&(
+ None,
+ tiled_pane_layout,
+ floating_pane_layout,
+ ))]
+ } else {
+ layout
+ .tabs()
+ .into_iter()
+ .map(|tab| TabMetadata::from(&tab))
+ .collect()
+ };
+
+ // Get file metadata for creation and modification times as Unix epochs
+ let (creation_time, update_time) =
+ LayoutMetadata::creation_and_update_times(&path);
+
+ LayoutMetadata {
+ tabs,
+ creation_time,
+ update_time,
+ }
+ },
+ Err(e) => {
+ log::error!("Failed to parse layout: {}", e);
+ LayoutMetadata::default()
+ },
+ }
+ },
+ Err(e) => {
+ log::error!("Failed to read layout file: {}", e);
+ LayoutMetadata::default()
+ },
+ }
+ }
+}
+
+impl LayoutMetadata {
+ fn creation_and_update_times(path: &PathBuf) -> (String, String) {
+ // (creation_time, update_time) returns stringified unix epoch
+ match std::fs::metadata(path) {
+ Ok(metadata) => {
+ let creation_time = metadata
+ .created()
+ .ok()
+ .and_then(|t| {
+ t.duration_since(std::time::UNIX_EPOCH)
+ .ok()
+ .map(|d| d.as_secs().to_string())
+ })
+ .unwrap_or_default();
+
+ let update_time = metadata
+ .modified()
+ .ok()
+ .and_then(|t| {
+ t.duration_since(std::time::UNIX_EPOCH)
+ .ok()
+ .map(|d| d.as_secs().to_string())
+ })
+ .unwrap_or_default();
+
+ (creation_time, update_time)
+ },
+ Err(_) => (String::new(), String::new()),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub struct TabMetadata {
+ pub panes: Vec<PaneMetadata>,
+ pub name: Option<String>,
+}
+
+impl
+ From<&(
+ Option<String>,
+ crate::input::layout::TiledPaneLayout,
+ Vec<crate::input::layout::FloatingPaneLayout>,
+ )> for TabMetadata
+{
+ fn from(
+ tab: &(
+ Option<String>,
+ crate::input::layout::TiledPaneLayout,
+ Vec<crate::input::layout::FloatingPaneLayout>,
+ ),
+ ) -> Self {
+ let (tab_name, tiled_pane_layout, floating_panes) = tab;
+
+ // Collect panes from tiled layout (only leaf nodes are real panes)
+ let mut panes = Vec::new();
+ collect_leaf_panes(&tiled_pane_layout, &mut panes);
+
+ // Collect panes from floating panes
+ for floating_pane in floating_panes {
+ panes.push(PaneMetadata::from(floating_pane));
+ }
+
+ TabMetadata {
+ panes,
+ name: tab_name.clone(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub struct PaneMetadata {
+ pub name: Option<String>,
+ pub is_plugin: bool,
+ pub is_builtin_plugin: bool,
+}
+
+impl From<&crate::input::layout::TiledPaneLayout> for PaneMetadata {
+ fn from(pane: &crate::input::layout::TiledPaneLayout) -> Self {
+ let mut is_plugin = false;
+ let mut is_builtin_plugin = false;
+
+ // Try to get the name from the pane's name field first
+ let name = if let Some(ref name) = pane.name {
+ Some(name.clone())
+ } else if let Some(ref run) = pane.run {
+ // If no explicit name, glean it from the run configuration
+ match run {
+ Run::Command(cmd) => {
+ // Use the command name
+ Some(cmd.command.to_string_lossy().to_string())
+ },
+ Run::EditFile(path, _line, _cwd) => {
+ // Use the file name
+ path.file_name().map(|n| n.to_string_lossy().to_string())
+ },
+ Run::Plugin(plugin) => {
+ is_plugin = true;
+ is_builtin_plugin = plugin.is_builtin_plugin();
+ Some(plugin.location_string())
+ },
+ Run::Cwd(_) => None,
+ }
+ } else {
+ None
+ };
+
+ PaneMetadata {
+ name,
+ is_plugin,
+ is_builtin_plugin,
+ }
+ }
+}
+
+impl From<&crate::input::layout::FloatingPaneLayout> for PaneMetadata {
+ fn from(pane: &crate::input::layout::FloatingPaneLayout) -> Self {
+ let mut is_plugin = false;
+ let mut is_builtin_plugin = false;
+
+ // Try to get the name from the pane's name field first
+ let name = if let Some(ref name) = pane.name {
+ Some(name.clone())
+ } else if let Some(ref run) = pane.run {
+ // If no explicit name, glean it from the run configuration
+ match run {
+ Run::Command(cmd) => {
+ // Use the command name
+ Some(cmd.command.to_string_lossy().to_string())
+ },
+ Run::EditFile(path, _line, _cwd) => {
+ // Use the file name
+ path.file_name().map(|n| n.to_string_lossy().to_string())
+ },
+ Run::Plugin(plugin) => {
+ is_plugin = true;
+ is_builtin_plugin = match plugin {
+ crate::input::layout::RunPluginOrAlias::RunPlugin(run_plugin) => {
+ matches!(run_plugin.location, RunPluginLocation::Zellij(_))
+ },
+ crate::input::layout::RunPluginOrAlias::Alias(_) => false,
+ };
+ // Use the plugin location string
+ Some(plugin.location_string())
+ },
+ Run::Cwd(_) => None,
+ }
+ } else {
+ None
+ };
+
+ PaneMetadata {
+ name,
+ is_plugin,
+ is_builtin_plugin,
+ }
+ }
+}
+
+// Helper function to recursively collect leaf panes from TiledPaneLayout
+fn collect_leaf_panes(
+ pane: &crate::input::layout::TiledPaneLayout,
+ result: &mut Vec<PaneMetadata>,
+) {
+ if pane.children.is_empty() {
+ // This is a leaf node (actual pane)
+ result.push(PaneMetadata::from(pane));
+ } else {
+ // This is a container, recurse into children
+ for child in &pane.children {
+ collect_leaf_panes(child, result);
+ }
+ }
+}
+
impl LayoutInfo {
pub fn name(&self) -> &str {
match self {
LayoutInfo::BuiltIn(name) => &name,
- LayoutInfo::File(name) => &name,
+ LayoutInfo::File(name, _) => &name,
LayoutInfo::Url(url) => &url,
LayoutInfo::Stringified(layout) => &layout,
}
@@ -1734,7 +1999,7 @@ impl LayoutInfo {
pub fn is_builtin(&self) -> bool {
match self {
LayoutInfo::BuiltIn(_name) => true,
- LayoutInfo::File(_name) => false,
+ LayoutInfo::File(_name, _) => false,
LayoutInfo::Url(_url) => false,
LayoutInfo::Stringified(_stringified) => false,
}
@@ -1753,8 +2018,11 @@ impl LayoutInfo {
else {
return None;
};
+ let file_path = layout_dir.join(layout_path);
Some(LayoutInfo::File(
- layout_dir.join(layout_path).display().to_string(),
+ // layout_dir.join(layout_path).display().to_string(),
+ file_path.display().to_string(),
+ LayoutMetadata::from(&file_path),
))
} else if layout_path.starts_with("http://") || layout_path.starts_with("https://")
{
@@ -1768,8 +2036,6 @@ impl LayoutInfo {
}
}
-use std::hash::{Hash, Hasher};
-
#[allow(clippy::derive_hash_xor_eq)]
impl Hash for SessionInfo {
fn hash<H: Hasher>(&self, state: &mut H) {
@@ -2113,6 +2379,36 @@ pub enum GetPanePidResponse {
Err(String),
}
+#[derive(Debug, Clone, PartialEq)]
+pub enum GetFocusedPaneInfoResponse {
+ Ok { tab_index: usize, pane_id: PaneId },
+ Err(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum SaveLayoutResponse {
+ Ok(()),
+ Err(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum DeleteLayoutResponse {
+ Ok(()),
+ Err(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum RenameLayoutResponse {
+ Ok(()),
+ Err(String),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum EditLayoutResponse {
+ Ok(()),
+ Err(String),
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SelectedText {
pub start: Position,
@@ -2391,7 +2687,7 @@ pub struct ConnectToSession {
impl ConnectToSession {
pub fn apply_layout_dir(&mut self, layout_dir: &PathBuf) {
- if let Some(LayoutInfo::File(file_path)) = self.layout.as_mut() {
+ if let Some(LayoutInfo::File(file_path, _layout_metadata)) = self.layout.as_mut() {
*file_path = Path::join(layout_dir, &file_path)
.to_string_lossy()
.to_string();
@@ -2829,7 +3125,9 @@ pub enum PluginCommand {
KillSessions(Vec<String>), // one or more session names
ScanHostFolder(PathBuf), // TODO: rename to ScanHostFolder
WatchFilesystem,
- DumpSessionLayout,
+ DumpSessionLayout {
+ tab_index: Option<usize>,
+ },
CloseSelf,
NewTabsWithLayoutInfo(LayoutInfo),
Reconfigure(String, bool), // String -> stringified configuration, bool -> save configuration
@@ -2920,4 +3218,32 @@ pub enum PluginCommand {
// suppress_replaced_pane)
RunAction(Action, BTreeMap<String, String>),
CopyToClipboard(String), // text to copy
+ OverrideLayout(
+ LayoutInfo,
+ bool, // retain_existing_terminal_panes
+ bool, // retain_existing_plugin_panes
+ bool, // apply_only_to_active_tab,
+ BTreeMap<String, String>, // context
+ ),
+ SaveLayout {
+ layout_name: String,
+ layout_kdl: String,
+ overwrite: bool,
+ },
+ DeleteLayout {
+ layout_name: String,
+ },
+ RenameLayout {
+ old_layout_name: String,
+ new_layout_name: String,
+ },
+ EditLayout {
+ layout_name: String,
+ context: Context,
+ },
+ GenerateRandomName,
+ DumpLayout(String),
+ ParseLayout(String), // String contains raw KDL layout
+ GetLayoutDir,
+ GetFocusedPaneInfo,
}
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 78330d76..06f69e06 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -353,6 +353,7 @@ pub enum ScreenContext {
SerializeLayoutForResurrection,
RenameSession,
DumpLayoutToPlugin,
+ GetFocusedPaneInfo,
ListClientsMetadata,
Reconfigure,
RerunCommandPane,
@@ -469,6 +470,8 @@ pub enum PluginContext {
FailedToStartWebServer,
PaneRenderReport,
UserInput,
+ LayoutListUpdate,
+ RequestStateUpdateForPlugin,
}
/// 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 42ec2037..caeadf4d 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -3,7 +3,7 @@
pub use super::command::{OpenFilePayload, RunCommandAction};
use super::layout::{
FloatingPaneLayout, Layout, PluginAlias, RunPlugin, RunPluginLocation, RunPluginOrAlias,
- SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout,
+ SwapFloatingLayout, SwapTiledLayout, TabLayoutInfo, TiledPaneLayout,
};
use crate::cli::CliAction;
use crate::data::{
@@ -351,13 +351,10 @@ pub enum Action {
NextSwapLayout,
/// Override the layout of the active tab
OverrideLayout {
- tiled_layout: Option<TiledPaneLayout>,
- floating_layouts: Vec<FloatingPaneLayout>,
- swap_tiled_layouts: Option<Vec<SwapTiledLayout>>,
- swap_floating_layouts: Option<Vec<SwapFloatingLayout>>,
- tab_name: Option<String>,
+ tabs: Vec<TabLayoutInfo>,
retain_existing_terminal_panes: bool,
retain_existing_plugin_panes: bool,
+ apply_only_to_active_tab: bool,
},
/// Query all tab names
QueryTabNames,
@@ -989,6 +986,7 @@ impl Action {
layout_dir,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
} => {
// Determine layout_dir: CLI arg > config > default
let layout_dir = layout_dir
@@ -1041,31 +1039,41 @@ impl Action {
stringified_error
})?;
- // Extract layout from first tab or use default
- let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone());
- let swap_floating_layouts = Some(layout.swap_floating_layouts.clone());
-
- if layout.tabs.len() > 1 {
- return Err(format!("This layout has {} tabs, overriding only supports layouts with 0 or 1 tabs.", layout.tabs.len()));
- }
+ // Convert all tabs to Vec<TabLayoutInfo>
+ let tabs: Vec<TabLayoutInfo> = layout
+ .tabs
+ .iter()
+ .enumerate()
+ .map(|(index, (tab_name, tiled, floating))| TabLayoutInfo {
+ tab_index: index,
+ tab_name: tab_name.clone(),
+ tiled_layout: tiled.clone(),
+ floating_layouts: floating.clone(),
+ swap_tiled_layouts: Some(layout.swap_tiled_layouts.clone()),
+ swap_floating_layouts: Some(layout.swap_floating_layouts.clone()),
+ })
+ .collect();
- let (name, tiled_layout, floating_panes_layout) =
- if let Some(first_tab) = layout.tabs.get(0).take() {
- first_tab.clone()
- } else {
- let name = None;
- let (tiled_panes, floating_panes) = layout.new_tab();
- (name, tiled_panes, floating_panes)
- };
+ // If no tabs, create default tab
+ let tabs = if tabs.is_empty() {
+ let (tiled, floating) = layout.new_tab();
+ vec![TabLayoutInfo {
+ tab_index: 0,
+ tab_name: None,
+ tiled_layout: tiled,
+ floating_layouts: floating,
+ swap_tiled_layouts: Some(layout.swap_tiled_layouts),
+ swap_floating_layouts: Some(layout.swap_floating_layouts),
+ }]
+ } else {
+ tabs
+ };
Ok(vec![Action::OverrideLayout {
- tiled_layout: Some(tiled_layout),
- floating_layouts: floating_panes_layout,
- swap_tiled_layouts,
- swap_floating_layouts,
- tab_name: name,
+ tabs,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
}])
},
CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]),
diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs
index 1f407621..9882b8e6 100644
--- a/zellij-utils/src/input/config.rs
+++ b/zellij-utils/src/input/config.rs
@@ -1,4 +1,8 @@
use crate::data::Styling;
+
+#[cfg(not(target_family = "wasm"))]
+use crate::data::{LayoutInfo, LayoutWithError};
+
use miette::{Diagnostic, LabeledSpan, NamedSource, SourceCode};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
@@ -36,15 +40,40 @@ pub struct Config {
pub web_client: WebClientConfig,
}
-#[derive(Error, Debug)]
+#[derive(Error, Debug, Serialize, Deserialize)]
pub struct KdlError {
pub error_message: String,
+ #[serde(skip)]
pub src: Option<NamedSource>,
pub offset: Option<usize>,
pub len: Option<usize>,
pub help_message: Option<String>,
}
+impl Clone for KdlError {
+ fn clone(&self) -> Self {
+ KdlError {
+ error_message: self.error_message.clone(),
+ src: None, // NamedSource doesn't implement Clone, so we skip it
+ offset: self.offset,
+ len: self.len,
+ help_message: self.help_message.clone(),
+ }
+ }
+}
+
+impl PartialEq for KdlError {
+ fn eq(&self, other: &Self) -> bool {
+ // Compare everything except src (which doesn't implement PartialEq)
+ self.error_message == other.error_message
+ && self.offset == other.offset
+ && self.len == other.len
+ && self.help_message == other.help_message
+ }
+}
+
+impl Eq for KdlError {}
+
impl KdlError {
pub fn add_src(mut self, src_name: String, src_input: String) -> Self {
self.src = Some(NamedSource::new(src_name, src_input));
@@ -482,6 +511,72 @@ where
}
}
+#[cfg(not(target_family = "wasm"))]
+pub async fn watch_layout_dir_changes<F, Fut>(
+ layout_dir: PathBuf,
+ default_layout_name: Option<String>,
+ on_layout_change: F,
+) where
+ F: Fn(Vec<LayoutInfo>, Vec<LayoutWithError>) -> Fut + Send + 'static,
+ Fut: std::future::Future<Output = ()> + Send,
+{
+ use crate::input::layout::Layout;
+ use notify::{self, Config as WatcherConfig, Event, PollWatcher, RecursiveMode, Watcher};
+ use std::time::Duration;
+ use tokio::sync::mpsc;
+
+ loop {
+ if layout_dir.exists() {
+ let (tx, mut rx) = mpsc::unbounded_channel();
+
+ let mut watcher = match PollWatcher::new(
+ move |res: Result<Event, notify::Error>| {
+ let _ = tx.send(res);
+ },
+ WatcherConfig::default().with_poll_interval(Duration::from_secs(1)),
+ ) {
+ Ok(watcher) => watcher,
+ Err(_) => break,
+ };
+
+ if watcher
+ .watch(&layout_dir, RecursiveMode::Recursive)
+ .is_err()
+ {
+ break;
+ }
+
+ while let Some(event_result) = rx.recv().await {
+ match event_result {
+ Ok(event) => {
+ if event.kind.is_remove()
+ || event.kind.is_create()
+ || event.kind.is_modify()
+ {
+ tokio::time::sleep(Duration::from_millis(100)).await;
+
+ if !layout_dir.exists() {
+ break;
+ }
+
+ let (layouts, layout_errors) = Layout::list_available_layouts(
+ Some(layout_dir.clone()),
+ &default_layout_name,
+ );
+ on_layout_change(layouts, layout_errors).await;
+ }
+ },
+ Err(_) => break,
+ }
+ }
+ }
+
+ while !layout_dir.exists() {
+ tokio::time::sleep(Duration::from_secs(3)).await;
+ }
+ }
+}
+
#[cfg(test)]
mod config_test {
use super::*;
diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs
index d925f7c5..8ba81159 100644
--- a/zellij-utils/src/input/layout.rs
+++ b/zellij-utils/src/input/layout.rs
@@ -11,7 +11,7 @@
#[cfg(not(target_family = "wasm"))]
use crate::downloader::Downloader;
use crate::{
- data::{Direction, LayoutInfo},
+ data::{Direction, LayoutInfo, LayoutMetadata, LayoutParsingError, LayoutWithError},
home::{default_layout_dir, find_default_config_dir},
input::{
command::RunCommand,
@@ -229,6 +229,11 @@ impl RunPluginOrAlias {
},
}
}
+ pub fn is_builtin_plugin(&self) -> bool {
+ self.get_run_plugin()
+ .map(|r| matches!(r.location, RunPluginLocation::Zellij(_)))
+ .unwrap_or(false)
+ }
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
@@ -681,6 +686,17 @@ pub struct Layout {
pub swap_floating_layouts: Vec<SwapFloatingLayout>,
}
+/// Layout configuration for a single tab in multi-tab override
+#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
+pub struct TabLayoutInfo {
+ pub tab_index: usize,
+ pub tab_name: Option<String>,
+ pub tiled_layout: TiledPaneLayout,
+ pub floating_layouts: Vec<FloatingPaneLayout>,
+ pub swap_tiled_layouts: Option<Vec<SwapTiledLayout>>,
+ pub swap_floating_layouts: Option<Vec<SwapFloatingLayout>>,
+}
+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub enum PercentOrFixed {
Percent(usize), // 1 to 100
@@ -1189,39 +1205,70 @@ impl Layout {
pub fn list_available_layouts(
layout_dir: Option<PathBuf>,
default_layout_name: &Option<String>,
- ) -> Vec<LayoutInfo> {
- let mut available_layouts = layout_dir
+ ) -> (Vec<LayoutInfo>, Vec<LayoutWithError>) {
+ let (mut available_layouts, layouts_with_errors) = layout_dir
.clone()
.or_else(|| default_layout_dir())
- .and_then(|layout_dir| match std::fs::read_dir(layout_dir) {
- Ok(layout_files) => Some(layout_files),
+ .and_then(|layout_dir| match std::fs::read_dir(&layout_dir) {
+ Ok(layout_files) => Some((layout_files, layout_dir)),
Err(_) => None,
})
- .map(|layout_files| {
+ .map(|(layout_files, layout_dir)| {
let mut available_layouts = vec![];
+ let mut layouts_with_errors = vec![];
for file in layout_files {
if let Ok(file) = file {
if file.path().extension().map(|e| e.to_ascii_lowercase())
== Some(std::ffi::OsString::from("kdl"))
{
- if Layout::from_path_or_default_without_config(
+ let layout_name = file
+ .path()
+ .file_stem()
+ .and_then(|f| f.to_str())
+ .map(|f| f.to_string())
+ .unwrap_or_default();
+
+ match Layout::from_path_or_default_without_config(
Some(&file.path()),
- layout_dir.clone(),
- )
- .is_ok()
- {
- if let Some(file_name) = file.path().file_stem() {
+ Some(layout_dir.clone()),
+ ) {
+ Ok(_layout) => {
+ let file_path = layout_dir.join(file.path()); // TODO: do we
+ // need
+ // file_stem()
+ // here too?
available_layouts.push(LayoutInfo::File(
- file_name.to_string_lossy().to_string(),
+ layout_name,
+ LayoutMetadata::from(&file_path),
))
- }
+ },
+ Err(config_error) => {
+ let file_path = file.path();
+ let file_name =
+ file_path.to_str().unwrap_or("unknown").to_string();
+ let source_code =
+ std::fs::read_to_string(&file_path).unwrap_or_default();
+
+ let error = match config_error {
+ ConfigError::KdlError(kdl_err) => {
+ LayoutParsingError::KdlError {
+ kdl_error: kdl_err,
+ file_name,
+ source_code,
+ }
+ },
+ _ => LayoutParsingError::SyntaxError,
+ };
+ layouts_with_errors
+ .push(LayoutWithError { layout_name, error });
+ },
}
}
}
}
- available_layouts
+ (available_layouts, layouts_with_errors)
})
- .unwrap_or_else(Default::default);
+ .unwrap_or_else(|| (Default::default(), Default::default()));
let default_layout_name = default_layout_name
.as_ref()
.map(|d| d.as_str())
@@ -1242,7 +1289,7 @@ impl Layout {
a_name.cmp(&b_name)
}
});
- available_layouts
+ (available_layouts, layouts_with_errors)
}
pub fn from_layout_info(
layout_dir: &Option<PathBuf>,
@@ -1250,7 +1297,7 @@ impl Layout {
) -> Result<Layout, ConfigError> {
let mut should_start_layout_commands_suspended = false;
let (path_to_raw_layout, raw_layout, raw_swap_layouts) = match layout_info {
- LayoutInfo::File(layout_name_without_extension) => {
+ LayoutInfo::File(layout_name_without_extension, _layout_metadata) => {
let layout_dir = layout_dir.clone().or_else(|| default_layout_dir());
let (path_to_layout, stringified_layout, swap_layouts) =
Self::stringified_from_dir(
@@ -1293,7 +1340,7 @@ impl Layout {
) -> Result<(Layout, Config), ConfigError> {
let mut should_start_layout_commands_suspended = false;
let (path_to_raw_layout, raw_layout, raw_swap_layouts) = match layout_info {
- LayoutInfo::File(layout_name_without_extension) => {
+ LayoutInfo::File(layout_name_without_extension, _layout_metadata) => {
let layout_dir = layout_dir.clone().or_else(|| default_layout_dir());
let (path_to_layout, stringified_layout, swap_layouts) =
Self::stringified_from_dir(
@@ -1371,6 +1418,19 @@ impl Layout {
let raw_layout = String::new();
Ok(raw_layout)
}
+ pub fn from_path_without_config(layout_path: &PathBuf) -> Result<Layout, ConfigError> {
+ // (path_to_layout as String, stringified_layout, Option<path_to_swap_layout as String, stringified_swap_layout>)
+ let (path_to_layout, raw_layout, raw_swap_layouts) =
+ Layout::stringified_from_path(layout_path)?;
+ Layout::from_kdl(
+ &raw_layout,
+ Some(path_to_layout),
+ raw_swap_layouts
+ .as_ref()
+ .map(|(r, f)| (r.as_str(), f.as_str())),
+ None,
+ )
+ }
pub fn from_path_or_default(
layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>,
diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs
index 892c7a66..fb63a786 100644
--- a/zellij-utils/src/input/plugins.rs
+++ b/zellij-utils/src/input/plugins.rs
@@ -68,6 +68,7 @@ impl PluginConfig {
|| tag == "share"
|| tag == "multiple-select"
|| tag == "sequence"
+ || tag == "layout-manager"
{
Some(PluginConfig {
path: PathBuf::from(&tag),
diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs
index e803f51e..4ade64b6 100644
--- a/zellij-utils/src/ipc/protobuf_conversion.rs
+++ b/zellij-utils/src/ipc/protobuf_conversion.rs
@@ -5,9 +5,10 @@ use crate::{
ClientToServerMsg as ProtoClientToServerMsg, ColorRegistersMsg, ConfigFileUpdatedMsg,
ConnStatusMsg, ConnectedMsg, DetachSessionMsg, ExitMsg, ExitReason as ProtoExitReason,
FailedToStartWebServerMsg, FirstClientConnectedMsg, ForegroundColorMsg,
- InputMode as ProtoInputMode, KeyMsg, KillSessionMsg, LogErrorMsg, LogMsg,
- QueryTerminalSizeMsg, RenamedSessionMsg, RenderMsg,
- ServerToClientMsg as ProtoServerToClientMsg, StartWebServerMsg, SwitchSessionMsg,
+ InputMode as ProtoInputMode, KeyMsg, KillSessionMsg, LayoutMetadata as ProtoLayoutMetadata,
+ LogErrorMsg, LogMsg, PaneMetadata as ProtoPaneMetadata, QueryTerminalSizeMsg,
+ RenamedSessionMsg, RenderMsg, ServerToClientMsg as ProtoServerToClientMsg,
+ StartWebServerMsg, SwitchSessionMsg, TabMetadata as ProtoTabMetadata,
TerminalPixelDimensionsMsg, TerminalResizeMsg, UnblockCliPipeInputMsg,
UnblockInputThreadMsg, WebServerStartedMsg,
},
@@ -1098,25 +1099,15 @@ impl From<crate::input::actions::Action>
ActionType::NextSwapLayout(NextSwapLayoutAction {})
},
crate::input::actions::Action::OverrideLayout {
- tiled_layout,
- floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
- tab_name,
+ tabs,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
} => ActionType::OverrideLayout(OverrideLayoutAction {
- tiled_layout: tiled_layout.map(|l| l.into()),
- floating_layouts: floating_layouts.into_iter().map(|l| l.into()).collect(),
- swap_tiled_layouts: swap_tiled_layouts
- .map(|layouts| layouts.into_iter().map(|l| l.into()).collect())
- .unwrap_or_default(),
- swap_floating_layouts: swap_floating_layouts
- .map(|layouts| layouts.into_iter().map(|l| l.into()).collect())
- .unwrap_or_default(),
- tab_name,
+ tabs: tabs.into_iter().map(|t| t.into()).collect(),
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
}),
crate::input::actions::Action::QueryTabNames => {
ActionType::QueryTabNames(QueryTabNamesAction {})
@@ -1695,45 +1686,16 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action>
ActionType::NextSwapLayout(_) => Ok(crate::input::actions::Action::NextSwapLayout),
ActionType::OverrideLayout(override_layout_action) => {
Ok(crate::input::actions::Action::OverrideLayout {
- tiled_layout: override_layout_action
- .tiled_layout
- .map(|l| l.try_into())
- .transpose()?,
- floating_layouts: override_layout_action
- .floating_layouts
+ tabs: override_layout_action
+ .tabs
.into_iter()
- .map(|l| l.try_into())
+ .map(|t| t.try_into())
.collect::<Result<Vec<_>>>()?,
- swap_tiled_layouts: if override_layout_action.swap_tiled_layouts.is_empty() {
- None
- } else {
- Some(
- override_layout_action
- .swap_tiled_layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>>>()?,
- )
- },
- swap_floating_layouts: if override_layout_action
- .swap_floating_layouts
- .is_empty()
- {
- None
- } else {
- Some(
- override_layout_action
- .swap_floating_layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>>>()?,
- )
- },
- tab_name: override_layout_action.tab_name,
retain_existing_terminal_panes: override_layout_action
.retain_existing_terminal_panes,
retain_existing_plugin_panes: override_layout_action
.retain_existing_plugin_panes,
+ apply_only_to_active_tab: override_layout_action.apply_only_to_active_tab,
})
},
ActionType::QueryTabNames(_) => Ok(crate::input::actions::Action::QueryTabNames),
@@ -2031,14 +1993,19 @@ impl From<crate::data::LayoutInfo>
{
fn from(layout: crate::data::LayoutInfo) -> Self {
use crate::client_server_contract::client_server_contract::layout_info::LayoutType;
- let layout_type = match layout {
- crate::data::LayoutInfo::BuiltIn(name) => LayoutType::BuiltinName(name),
- crate::data::LayoutInfo::File(path) => LayoutType::FilePath(path),
- crate::data::LayoutInfo::Url(url) => LayoutType::Url(url),
- crate::data::LayoutInfo::Stringified(content) => LayoutType::Stringified(content),
+ let (layout_type, layout_metadata) = match layout {
+ crate::data::LayoutInfo::BuiltIn(name) => (LayoutType::BuiltinName(name), None),
+ crate::data::LayoutInfo::File(path, metadata) => {
+ (LayoutType::FilePath(path), Some(metadata.into()))
+ },
+ crate::data::LayoutInfo::Url(url) => (LayoutType::Url(url), None),
+ crate::data::LayoutInfo::Stringified(content) => {
+ (LayoutType::Stringified(content), None)
+ },
};
Self {
layout_type: Some(layout_type),
+ layout_metadata,
}
}
}
@@ -2053,7 +2020,14 @@ impl TryFrom<crate::client_server_contract::client_server_contract::LayoutInfo>
use crate::client_server_contract::client_server_contract::layout_info::LayoutType;
match layout.layout_type {
Some(LayoutType::BuiltinName(name)) => Ok(crate::data::LayoutInfo::BuiltIn(name)),
- Some(LayoutType::FilePath(path)) => Ok(crate::data::LayoutInfo::File(path)),
+ Some(LayoutType::FilePath(path)) => {
+ let layout_metadata = layout
+ .layout_metadata
+ .map(|m| m.try_into())
+ .transpose()?
+ .unwrap_or_default();
+ Ok(crate::data::LayoutInfo::File(path, layout_metadata))
+ },
Some(LayoutType::Url(url)) => Ok(crate::data::LayoutInfo::Url(url)),
Some(LayoutType::Stringified(content)) => {
Ok(crate::data::LayoutInfo::Stringified(content))
@@ -2063,6 +2037,77 @@ impl TryFrom<crate::client_server_contract::client_server_contract::LayoutInfo>
}
}
+impl From<crate::data::LayoutMetadata> for ProtoLayoutMetadata {
+ fn from(metadata: crate::data::LayoutMetadata) -> Self {
+ ProtoLayoutMetadata {
+ tabs: metadata.tabs.into_iter().map(|t| t.into()).collect(),
+ creation_time: metadata.creation_time,
+ update_time: metadata.update_time,
+ }
+ }
+}
+
+impl TryFrom<ProtoLayoutMetadata> for crate::data::LayoutMetadata {
+ type Error = anyhow::Error;
+ fn try_from(proto_metadata: ProtoLayoutMetadata) -> Result<Self> {
+ let tabs = proto_metadata
+ .tabs
+ .into_iter()
+ .map(|t| t.try_into())
+ .collect::<Result<Vec<_>>>()?;
+ Ok(crate::data::LayoutMetadata {
+ tabs,
+ creation_time: proto_metadata.creation_time,
+ update_time: proto_metadata.update_time,
+ })
+ }
+}
+
+impl From<crate::data::TabMetadata> for ProtoTabMetadata {
+ fn from(metadata: crate::data::TabMetadata) -> Self {
+ ProtoTabMetadata {
+ pane_metadata: metadata.panes.into_iter().map(|p| p.into()).collect(),
+ name: metadata.name,
+ }
+ }
+}
+
+impl TryFrom<ProtoTabMetadata> for crate::data::TabMetadata {
+ type Error = anyhow::Error;
+ fn try_from(proto_metadata: ProtoTabMetadata) -> Result<Self> {
+ let panes = proto_metadata
+ .pane_metadata
+ .into_iter()
+ .map(|p| p.try_into())
+ .collect::<Result<Vec<_>>>()?;
+ Ok(crate::data::TabMetadata {
+ panes,
+ name: proto_metadata.name,
+ })
+ }
+}
+
+impl From<crate::data::PaneMetadata> for ProtoPaneMetadata {
+ fn from(metadata: crate::data::PaneMetadata) -> Self {
+ ProtoPaneMetadata {
+ name: metadata.name,
+ is_plugin: metadata.is_plugin,
+ is_builtin_plugin: metadata.is_builtin_plugin,
+ }
+ }
+}
+
+impl TryFrom<ProtoPaneMetadata> for crate::data::PaneMetadata {
+ type Error = anyhow::Error;
+ fn try_from(proto_metadata: ProtoPaneMetadata) -> Result<Self> {
+ Ok(crate::data::PaneMetadata {
+ name: proto_metadata.name,
+ is_plugin: proto_metadata.is_plugin,
+ is_builtin_plugin: proto_metadata.is_builtin_plugin,
+ })
+ }
+}
+
impl From<ExitReason> for ProtoExitReason {
fn from(reason: ExitReason) -> Self {
match reason {
@@ -2686,6 +2731,82 @@ impl From<crate::input::layout::Run>
}
}
+// TabLayoutInfo conversion
+impl From<crate::input::layout::TabLayoutInfo>
+ for crate::client_server_contract::client_server_contract::TabLayoutInfo
+{
+ fn from(tab_info: crate::input::layout::TabLayoutInfo) -> Self {
+ Self {
+ tab_index: tab_info.tab_index as u32,
+ tab_name: tab_info.tab_name,
+ tiled_layout: Some(tab_info.tiled_layout.into()),
+ floating_layouts: tab_info
+ .floating_layouts
+ .into_iter()
+ .map(|l| l.into())
+ .collect(),
+ swap_tiled_layouts: tab_info
+ .swap_tiled_layouts
+ .unwrap_or_default()
+ .into_iter()
+ .map(|l| l.into())
+ .collect(),
+ swap_floating_layouts: tab_info
+ .swap_floating_layouts
+ .unwrap_or_default()
+ .into_iter()
+ .map(|l| l.into())
+ .collect(),
+ }
+ }
+}
+
+impl TryFrom<crate::client_server_contract::client_server_contract::TabLayoutInfo>
+ for crate::input::layout::TabLayoutInfo
+{
+ type Error = anyhow::Error;
+
+ fn try_from(
+ protobuf_tab: crate::client_server_contract::client_server_contract::TabLayoutInfo,
+ ) -> Result<Self> {
+ Ok(crate::input::layout::TabLayoutInfo {
+ tab_index: protobuf_tab.tab_index as usize,
+ tab_name: protobuf_tab.tab_name.filter(|s| !s.is_empty()),
+ tiled_layout: protobuf_tab
+ .tiled_layout
+ .ok_or_else(|| anyhow!("missing tiled_layout"))?
+ .try_into()?,
+ floating_layouts: protobuf_tab
+ .floating_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>>>()?,
+ swap_tiled_layouts: if protobuf_tab.swap_tiled_layouts.is_empty() {
+ None
+ } else {
+ Some(
+ protobuf_tab
+ .swap_tiled_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>>>()?,
+ )
+ },
+ swap_floating_layouts: if protobuf_tab.swap_floating_layouts.is_empty() {
+ None
+ } else {
+ Some(
+ protobuf_tab
+ .swap_floating_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>>>()?,
+ )
+ },
+ })
+ }
+}
+
// TiledPaneLayout conversion
impl From<crate::input::layout::TiledPaneLayout>
for crate::client_server_contract::client_server_contract::TiledPaneLayout
diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs
index 216c6e14..01275198 100644
--- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs
+++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs
@@ -1,8 +1,8 @@
use super::test_framework::*;
use crate::data::{
BareKey, CommandOrPlugin, ConnectToSession, Direction, FloatingPaneCoordinates, InputMode,
- KeyModifier, KeyWithModifier, LayoutInfo, OriginatingPlugin, PaneId, PluginTag, Resize,
- WebSharing,
+ KeyModifier, KeyWithModifier, LayoutInfo, LayoutMetadata, OriginatingPlugin, PaneId, PluginTag,
+ Resize, WebSharing,
};
use crate::input::actions::{Action, SearchDirection, SearchOption};
use crate::input::cli_assets::CliAssets;
@@ -2054,13 +2054,10 @@ fn test_client_messages() {
});
test_client_roundtrip!(ClientToServerMsg::Action {
action: Action::OverrideLayout {
- tiled_layout: None,
- floating_layouts: vec![],
- swap_tiled_layouts: None,
- swap_floating_layouts: None,
- tab_name: None,
+ tabs: vec![],
retain_existing_terminal_panes: false,
retain_existing_plugin_panes: false,
+ apply_only_to_active_tab: false,
},
terminal_id: Some(1),
client_id: Some(100),
@@ -3050,7 +3047,14 @@ fn test_server_messages() {
name: Some("new_session_name".to_owned()),
tab_position: Some(5),
pane_id: Some((5, true)),
- layout: Some(LayoutInfo::File("/path/to/my/file.kdl".to_owned())),
+ layout: Some(LayoutInfo::File(
+ "/path/to/my/file.kdl".to_owned(),
+ LayoutMetadata {
+ tabs: vec![],
+ creation_time: "0".to_owned(),
+ update_time: "0".to_owned()
+ }
+ )),
cwd: Some(PathBuf::from("/path/to/cwd")),
}
});
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index 7603af3f..95d67ffd 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -1,15 +1,16 @@
mod kdl_layout_parser;
use crate::data::{
BareKey, Direction, FloatingPaneCoordinates, InputMode, KeyWithModifier, LayoutInfo,
- MultiplayerColors, Palette, PaletteColor, PaneId, PaneInfo, PaneManifest, PermissionType,
- Resize, SessionInfo, StyleDeclaration, Styling, TabInfo, WebSharing, DEFAULT_STYLES,
+ LayoutMetadata, 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};
use crate::input::config::{Config, ConfigError, KdlError};
use crate::input::keybinds::Keybinds;
use crate::input::layout::{
- Layout, PluginUserConfiguration, RunPlugin, RunPluginOrAlias, SplitSize,
+ Layout, PluginUserConfiguration, RunPlugin, RunPluginOrAlias, SplitSize, TabLayoutInfo,
};
use crate::input::options::{Clipboard, OnForceClose, Options};
use crate::input::permission::{GrantedPermission, PermissionCache};
@@ -1727,13 +1728,10 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
let command_metadata = action_children.iter().next();
if command_metadata.is_none() {
return Ok(Action::OverrideLayout {
- tiled_layout: None,
- floating_layouts: vec![],
- swap_tiled_layouts: None,
- swap_floating_layouts: None,
- tab_name: None,
+ tabs: vec![],
retain_existing_terminal_panes: false,
retain_existing_plugin_panes: false,
+ apply_only_to_active_tab: false,
});
}
@@ -1760,6 +1758,9 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
kdl_child_bool_value_for_entry(c_m, "retain_existing_plugin_panes")
})
.unwrap_or(false);
+ let apply_only_to_active_tab = command_metadata
+ .and_then(|c_m| kdl_child_bool_value_for_entry(c_m, "apply_only_to_active_tab"))
+ .unwrap_or(false);
let layout_dir = config_options
.layout_dir
@@ -1804,26 +1805,38 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
let (tab_name, layout, floating_panes_layout) = tabs.drain(..).next().unwrap();
let name = tab_name.or(name);
- Ok(Action::OverrideLayout {
- tiled_layout: Some(layout),
+ let tab_layout_info = TabLayoutInfo {
+ tab_index: 0,
+ tab_name: name,
+ tiled_layout: layout,
floating_layouts: floating_panes_layout,
swap_tiled_layouts,
swap_floating_layouts,
- tab_name: name,
+ };
+
+ Ok(Action::OverrideLayout {
+ tabs: vec![tab_layout_info],
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
})
} else {
let (layout, floating_panes_layout) = layout.new_tab();
- Ok(Action::OverrideLayout {
- tiled_layout: Some(layout),
+ let tab_layout_info = TabLayoutInfo {
+ tab_index: 0,
+ tab_name: name,
+ tiled_layout: layout,
floating_layouts: floating_panes_layout,
swap_tiled_layouts,
swap_floating_layouts,
- tab_name: name,
+ };
+
+ Ok(Action::OverrideLayout {
+ tabs: vec![tab_layout_info],
retain_existing_terminal_panes,
retain_existing_plugin_panes,
+ apply_only_to_active_tab,
})
}
},
@@ -5154,7 +5167,9 @@ impl SessionInfo {
match layout_source {
Some(layout_source) => match layout_source {
"built-in" => Some(LayoutInfo::BuiltIn(layout_name)),
- "file" => Some(LayoutInfo::File(layout_name)),
+ "file" => {
+ Some(LayoutInfo::File(layout_name, LayoutMetadata::default()))
+ },
_ => None,
},
None => None,
@@ -5288,7 +5303,7 @@ impl SessionInfo {
let mut available_layouts_children = KdlDocument::new();
for layout_info in &self.available_layouts {
let (layout_name, layout_source) = match layout_info {
- LayoutInfo::File(name) => (name.clone(), "file"),
+ LayoutInfo::File(name, _layout_metadata) => (name.clone(), "file"),
LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"),
LayoutInfo::Url(url) => (url.clone(), "url"),
LayoutInfo::Stringified(_stringified) => ("stringified-layout".to_owned(), "N/A"),
@@ -5906,9 +5921,9 @@ fn serialize_and_deserialize_session_info_with_data() {
connected_clients: 2,
is_current_session: false,
available_layouts: vec![
- LayoutInfo::File("layout1".to_owned()),
+ LayoutInfo::File("layout1".to_owned(), LayoutMetadata::default()),
LayoutInfo::BuiltIn("layout2".to_owned()),
- LayoutInfo::File("layout3".to_owned()),
+ LayoutInfo::File("layout3".to_owned(), LayoutMetadata::default()),
],
plugins: Default::default(),
web_client_count: 2,
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 6353ee0e..9fa94e0b 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
@@ -114,6 +114,13 @@ keybinds clear-defaults=true {
}
SwitchToMode "normal"
}
+ bind "l" {
+ LaunchOrFocusPlugin "zellij:layout-manager" {
+ floating true
+ move_to_focused_tab true
+ }
+ SwitchToMode "normal"
+ }
bind "Ctrl o" { SwitchToMode "normal"; }
bind "p" {
LaunchOrFocusPlugin "plugin-manager" {
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 e562e030..c9d93654 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
@@ -114,6 +114,13 @@ keybinds clear-defaults=true {
}
SwitchToMode "normal"
}
+ bind "l" {
+ LaunchOrFocusPlugin "zellij:layout-manager" {
+ floating true
+ move_to_focused_tab true
+ }
+ SwitchToMode "normal"
+ }
bind "Ctrl o" { SwitchToMode "normal"; }
bind "p" {
LaunchOrFocusPlugin "plugin-manager" {
diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto
index de8ef44f..6b899b48 100644
--- a/zellij-utils/src/plugin_api/action.proto
+++ b/zellij-utils/src/plugin_api/action.proto
@@ -162,14 +162,20 @@ message NewTabPayload {
optional UnblockCondition first_pane_unblock_condition = 9;
}
+message TabLayoutInfo {
+ uint32 tab_index = 1;
+ optional string tab_name = 2;
+ TiledPaneLayout tiled_layout = 3;
+ repeated FloatingPaneLayout floating_layouts = 4;
+ repeated SwapTiledLayout swap_tiled_layouts = 5;
+ repeated SwapFloatingLayout swap_floating_layouts = 6;
+}
+
message OverrideLayoutPayload {
- 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 retain_existing_terminal_panes = 10;
- bool retain_existing_plugin_panes = 11;
+ repeated TabLayoutInfo tabs = 1;
+ bool retain_existing_terminal_panes = 2;
+ bool retain_existing_plugin_panes = 3;
+ bool apply_only_to_active_tab = 4;
}
message Action {
diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs
index 860b090b..aa4c8b09 100644
--- a/zellij-utils/src/plugin_api/action.rs
+++ b/zellij-utils/src/plugin_api/action.rs
@@ -62,6 +62,7 @@ pub use super::generated_api::api::{
SwapFloatingLayout as ProtobufSwapFloatingLayout,
SwapTiledLayout as ProtobufSwapTiledLayout,
SwitchToModePayload,
+ TabLayoutInfo as ProtobufTabLayoutInfo,
TiledPaneLayout as ProtobufTiledPaneLayout,
TiledPlacement as ProtobufTiledPlacement,
UnblockCondition as ProtobufUnblockCondition,
@@ -83,7 +84,7 @@ use crate::input::layout::SplitSize;
use crate::input::layout::{
FloatingPaneLayout, LayoutConstraint, PercentOrFixed, PluginAlias, PluginUserConfiguration,
Run, RunPlugin, RunPluginLocation, RunPluginOrAlias, SplitDirection, SwapFloatingLayout,
- SwapTiledLayout, TiledPaneLayout,
+ SwapTiledLayout, TabLayoutInfo, TiledPaneLayout,
};
use crate::input::mouse::{MouseEvent, MouseEventType};
use crate::position::Position;
@@ -723,48 +724,15 @@ impl TryFrom<ProtobufAction> for Action {
},
Some(ProtobufActionName::OverrideLayout) => match protobuf_action.optional_payload {
Some(OptionalPayload::OverrideLayoutPayload(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.filter(|s| !s.is_empty());
-
Ok(Action::OverrideLayout {
- tiled_layout,
- floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
- tab_name,
+ tabs: payload
+ .tabs
+ .into_iter()
+ .map(|t| t.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
retain_existing_terminal_panes: payload.retain_existing_terminal_panes,
retain_existing_plugin_panes: payload.retain_existing_plugin_panes,
+ apply_only_to_active_tab: payload.apply_only_to_active_tab,
})
},
Some(_) => Err("Mismatched payload for OverrideLayout"),
@@ -1556,56 +1524,24 @@ impl TryFrom<Action> for ProtobufAction {
optional_payload: None,
}),
Action::OverrideLayout {
- tiled_layout,
- floating_layouts,
- swap_tiled_layouts,
- swap_floating_layouts,
- tab_name,
+ tabs,
retain_existing_terminal_panes,
retain_existing_plugin_panes,
- } => {
- let protobuf_tiled_layout = tiled_layout.map(|l| l.try_into()).transpose()?;
-
- let protobuf_floating_layouts = floating_layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>, _>>()?;
-
- let protobuf_swap_tiled_layouts = swap_tiled_layouts
- .map(|layouts| {
- layouts
- .into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>, _>>()
- })
- .transpose()?
- .unwrap_or_default();
-
- let protobuf_swap_floating_layouts = swap_floating_layouts
- .map(|layouts| {
- layouts
+ apply_only_to_active_tab,
+ } => Ok(ProtobufAction {
+ name: ProtobufActionName::OverrideLayout as i32,
+ optional_payload: Some(OptionalPayload::OverrideLayoutPayload(
+ OverrideLayoutPayload {
+ tabs: tabs
.into_iter()
- .map(|l| l.try_into())
- .collect::<Result<Vec<_>, _>>()
- })
- .transpose()?
- .unwrap_or_default();
-
- Ok(ProtobufAction {
- name: ProtobufActionName::OverrideLayout as i32,
- optional_payload: Some(OptionalPayload::OverrideLayoutPayload(
- OverrideLayoutPayload {
- 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(),
- retain_existing_terminal_panes,
- retain_existing_plugin_panes,
- },
- )),
- })
- },
+ .map(|t| t.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ apply_only_to_active_tab,
+ },
+ )),
+ }),
Action::QueryTabNames => Ok(ProtobufAction {
name: ProtobufActionName::QueryTabNames as i32,
optional_payload: None,
@@ -2739,6 +2675,77 @@ impl TryFrom<CommandOrPlugin> for ProtobufCommandOrPlugin {
}
}
+impl TryFrom<ProtobufTabLayoutInfo> for TabLayoutInfo {
+ type Error = &'static str;
+
+ fn try_from(protobuf_tab: ProtobufTabLayoutInfo) -> Result<Self, Self::Error> {
+ Ok(TabLayoutInfo {
+ tab_index: protobuf_tab.tab_index as usize,
+ tab_name: protobuf_tab.tab_name.filter(|s| !s.is_empty()),
+ tiled_layout: protobuf_tab
+ .tiled_layout
+ .ok_or("missing tiled_layout")?
+ .try_into()?,
+ floating_layouts: protobuf_tab
+ .floating_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ swap_tiled_layouts: if protobuf_tab.swap_tiled_layouts.is_empty() {
+ None
+ } else {
+ Some(
+ protobuf_tab
+ .swap_tiled_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ )
+ },
+ swap_floating_layouts: if protobuf_tab.swap_floating_layouts.is_empty() {
+ None
+ } else {
+ Some(
+ protobuf_tab
+ .swap_floating_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ )
+ },
+ })
+ }
+}
+
+impl TryFrom<TabLayoutInfo> for ProtobufTabLayoutInfo {
+ type Error = &'static str;
+
+ fn try_from(tab_info: TabLayoutInfo) -> Result<Self, Self::Error> {
+ Ok(ProtobufTabLayoutInfo {
+ tab_index: tab_info.tab_index as u32,
+ tab_name: tab_info.tab_name,
+ tiled_layout: Some(tab_info.tiled_layout.try_into()?),
+ floating_layouts: tab_info
+ .floating_layouts
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ swap_tiled_layouts: tab_info
+ .swap_tiled_layouts
+ .unwrap_or_default()
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ swap_floating_layouts: tab_info
+ .swap_floating_layouts
+ .unwrap_or_default()
+ .into_iter()
+ .map(|l| l.try_into())
+ .collect::<Result<Vec<_>, _>>()?,
+ })
+ }
+}
+
impl TryFrom<ProtobufTiledPaneLayout> for TiledPaneLayout {
type Error = &'static str;
fn try_from(protobuf: ProtobufTiledPaneLayout) -> Result<Self, Self::Error> {
diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto
index 3bdeea19..10125387 100644
--- a/zellij-utils/src/plugin_api/event.proto
+++ b/zellij-utils/src/plugin_api/event.proto
@@ -62,6 +62,7 @@ enum EventType {
UserAction = 37;
ActionComplete = 38;
CwdChanged = 39;
+ AvailableLayoutInfo = 40;
}
message EventNameList {
@@ -103,6 +104,7 @@ message Event {
UserActionPayload user_action_payload = 31;
ActionCompletePayload action_complete_payload = 32;
CwdChangedPayload cwd_changed_payload = 33;
+ AvailableLayoutInfoPayload available_layout_info_payload = 34;
}
}
@@ -202,6 +204,11 @@ message SessionUpdatePayload {
repeated ResurrectableSession resurrectable_sessions = 2;
}
+message AvailableLayoutInfoPayload {
+ repeated LayoutInfo available_layouts = 1;
+ repeated LayoutWithError layouts_with_errors = 2;
+}
+
message RunCommandResultPayload {
optional int32 exit_code = 1;
bytes stdout = 2;
@@ -318,6 +325,54 @@ message PluginInfo {
message LayoutInfo {
string name = 1;
string source = 2;
+ optional LayoutMetadata layout_metadata = 5;
+}
+
+message LayoutWithError {
+ string layout_name = 1;
+ LayoutParsingError error = 2;
+}
+
+message LayoutParsingError {
+ oneof error_type {
+ KdlErrorVariant kdl_error = 1;
+ SyntaxError syntax_error = 2;
+ }
+}
+
+message KdlErrorVariant {
+ KdlError kdl_error = 1;
+ string file_name = 2;
+ string source_code = 3;
+}
+
+message SyntaxError {
+ // Empty message, just a marker
+}
+
+message KdlError {
+ string error_message = 1;
+ optional uint64 offset = 2;
+ optional uint64 len = 3;
+ optional string help_message = 4;
+}
+
+message LayoutMetadata {
+ repeated TabMetadata tabs = 1;
+ string creation_time = 2;
+ string update_time = 3;
+
+}
+
+message TabMetadata {
+ repeated PaneMetadata pane_metadata = 1;
+ optional string name = 2;
+}
+
+message PaneMetadata {
+ optional string name = 1;
+ bool is_plugin = 2;
+ bool is_builtin_plugin = 3;
}
message ResurrectableSession {
diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs
index 4e430b0b..80d77e56 100644
--- a/zellij-utils/src/plugin_api/event.rs
+++ b/zellij-utils/src/plugin_api/event.rs
@@ -1,23 +1,29 @@
pub use super::generated_api::api::{
action::{Action as ProtobufAction, Position as ProtobufPosition},
event::{
- event::Payload as ProtobufEventPayload, pane_scrollback_response,
- ActionCompletePayload as ProtobufActionCompletePayload, ClientInfo as ProtobufClientInfo,
- ClientPaneHistory as ProtobufClientPaneHistory,
+ event::Payload as ProtobufEventPayload,
+ layout_parsing_error::ErrorType as ProtobufLayoutParsingErrorType,
+ pane_scrollback_response, ActionCompletePayload as ProtobufActionCompletePayload,
+ AvailableLayoutInfoPayload as ProtobufAvailableLayoutInfoPayload,
+ 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,
+ InputModeKeybinds as ProtobufInputModeKeybinds, KdlError as ProtobufKdlError,
+ KdlErrorVariant as ProtobufKdlErrorVariant, KeyBind as ProtobufKeyBind,
+ LayoutInfo as ProtobufLayoutInfo, LayoutMetadata as ProtobufLayoutMetadata,
+ LayoutParsingError as ProtobufLayoutParsingError,
+ LayoutWithError as ProtobufLayoutWithError, ModeUpdatePayload as ProtobufModeUpdatePayload,
PaneContents as ProtobufPaneContents, PaneContentsEntry as ProtobufPaneContentsEntry,
PaneId as ProtobufPaneId, PaneInfo as ProtobufPaneInfo,
- PaneManifest as ProtobufPaneManifest,
+ PaneManifest as ProtobufPaneManifest, PaneMetadata as ProtobufPaneMetadata,
PaneRenderReportPayload as ProtobufPaneRenderReportPayload,
PaneScrollbackResponse as ProtobufPaneScrollbackResponse, PaneType as ProtobufPaneType,
PluginInfo as ProtobufPluginInfo, ResurrectableSession as ProtobufResurrectableSession,
SelectedText as ProtobufSelectedText, SessionManifest as ProtobufSessionManifest,
- TabInfo as ProtobufTabInfo, UserActionPayload as ProtobufUserActionPayload,
+ SyntaxError as ProtobufSyntaxError, TabInfo as ProtobufTabInfo,
+ TabMetadata as ProtobufTabMetadata, UserActionPayload as ProtobufUserActionPayload,
WebServerStatusPayload as ProtobufWebServerStatusPayload, WebSharing as ProtobufWebSharing,
*,
},
@@ -28,9 +34,10 @@ pub use super::generated_api::api::{
#[allow(hidden_glob_reexports)]
use crate::data::{
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,
+ KeyWithModifier, LayoutInfo, LayoutMetadata, ModeInfo, Mouse, PaneContents, PaneId, PaneInfo,
+ PaneManifest, PaneMetadata, PaneScrollbackResponse, PermissionStatus, PluginCapabilities,
+ PluginInfo, SelectedText, SessionInfo, Style, TabInfo, TabMetadata, WebServerStatus,
+ WebSharing,
};
use crate::errors::prelude::*;
@@ -458,6 +465,29 @@ impl TryFrom<ProtobufEvent> for Event {
},
_ => Err("Malformed payload for the CwdChanged Event"),
},
+ Some(ProtobufEventType::AvailableLayoutInfo) => match protobuf_event.payload {
+ Some(ProtobufEventPayload::AvailableLayoutInfoPayload(
+ available_layout_info_payload,
+ )) => {
+ let mut available_layouts: Vec<LayoutInfo> = vec![];
+ let mut layouts_with_errors: Vec<crate::data::LayoutWithError> = vec![];
+
+ for protobuf_layout_info in available_layout_info_payload.available_layouts {
+ available_layouts.push(LayoutInfo::try_from(protobuf_layout_info)?);
+ }
+
+ for protobuf_error in available_layout_info_payload.layouts_with_errors {
+ layouts_with_errors
+ .push(crate::data::LayoutWithError::try_from(protobuf_error)?);
+ }
+
+ Ok(Event::AvailableLayoutInfo(
+ available_layouts,
+ layouts_with_errors,
+ ))
+ },
+ _ => Err("Malformed payload for the AvailableLayoutInfo Event"),
+ },
None => Err("Unknown Protobuf Event"),
}
}
@@ -910,6 +940,30 @@ impl TryFrom<Event> for ProtobufEvent {
payload: Some(event::Payload::CwdChangedPayload(cwd_changed_payload)),
})
},
+ Event::AvailableLayoutInfo(available_layouts, layouts_with_errors) => {
+ let mut protobuf_available_layouts = vec![];
+ let mut protobuf_layouts_with_errors = vec![];
+
+ for layout_info in available_layouts {
+ protobuf_available_layouts.push(layout_info.try_into()?);
+ }
+
+ for layout_error in layouts_with_errors {
+ protobuf_layouts_with_errors.push(layout_error.try_into()?);
+ }
+
+ let available_layout_info_payload = ProtobufAvailableLayoutInfoPayload {
+ available_layouts: protobuf_available_layouts,
+ layouts_with_errors: protobuf_layouts_with_errors,
+ };
+
+ Ok(ProtobufEvent {
+ name: ProtobufEventType::AvailableLayoutInfo as i32,
+ payload: Some(event::Payload::AvailableLayoutInfoPayload(
+ available_layout_info_payload,
+ )),
+ })
+ },
}
}
}
@@ -1078,21 +1132,25 @@ impl TryFrom<LayoutInfo> for ProtobufLayoutInfo {
type Error = &'static str;
fn try_from(layout_info: LayoutInfo) -> Result<Self, &'static str> {
match layout_info {
- LayoutInfo::File(name) => Ok(ProtobufLayoutInfo {
+ LayoutInfo::File(name, layout_metadata) => Ok(ProtobufLayoutInfo {
source: "file".to_owned(),
name,
+ layout_metadata: Some(layout_metadata.try_into()?),
}),
LayoutInfo::BuiltIn(name) => Ok(ProtobufLayoutInfo {
source: "built-in".to_owned(),
name,
+ layout_metadata: None,
}),
LayoutInfo::Url(name) => Ok(ProtobufLayoutInfo {
source: "url".to_owned(),
name,
+ layout_metadata: None,
}),
LayoutInfo::Stringified(stringified_layout) => Ok(ProtobufLayoutInfo {
source: "stringified".to_owned(),
name: stringified_layout.clone(),
+ layout_metadata: None,
}),
}
}
@@ -1102,7 +1160,14 @@ impl TryFrom<ProtobufLayoutInfo> for LayoutInfo {
type Error = &'static str;
fn try_from(protobuf_layout_info: ProtobufLayoutInfo) -> Result<Self, &'static str> {
match protobuf_layout_info.source.as_str() {
- "file" => Ok(LayoutInfo::File(protobuf_layout_info.name)),
+ "file" => {
+ let layout_metadata = protobuf_layout_info
+ .layout_metadata
+ .map(|m| m.try_into())
+ .transpose()?
+ .unwrap_or_default();
+ Ok(LayoutInfo::File(protobuf_layout_info.name, layout_metadata))
+ },
"built-in" => Ok(LayoutInfo::BuiltIn(protobuf_layout_info.name)),
"url" => Ok(LayoutInfo::Url(protobuf_layout_info.name)),
"stringified" => Ok(LayoutInfo::Stringified(protobuf_layout_info.name)),
@@ -1111,6 +1176,183 @@ impl TryFrom<ProtobufLayoutInfo> for LayoutInfo {
}
}
+impl TryFrom<ProtobufLayoutMetadata> for LayoutMetadata {
+ type Error = &'static str;
+ fn try_from(protobuf_metadata: ProtobufLayoutMetadata) -> Result<Self, &'static str> {
+ let tabs = protobuf_metadata
+ .tabs
+ .into_iter()
+ .map(|t| t.try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(LayoutMetadata {
+ tabs,
+ creation_time: protobuf_metadata.creation_time,
+ update_time: protobuf_metadata.update_time,
+ })
+ }
+}
+
+impl TryFrom<LayoutMetadata> for ProtobufLayoutMetadata {
+ type Error = &'static str;
+ fn try_from(metadata: LayoutMetadata) -> Result<Self, &'static str> {
+ let tabs = metadata
+ .tabs
+ .into_iter()
+ .map(|t| t.try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(ProtobufLayoutMetadata {
+ tabs,
+ creation_time: metadata.creation_time,
+ update_time: metadata.update_time,
+ })
+ }
+}
+
+impl TryFrom<ProtobufTabMetadata> for TabMetadata {
+ type Error = &'static str;
+ fn try_from(protobuf_metadata: ProtobufTabMetadata) -> Result<Self, &'static str> {
+ let panes = protobuf_metadata
+ .pane_metadata
+ .into_iter()
+ .map(|p| p.try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(TabMetadata {
+ panes,
+ name: protobuf_metadata.name,
+ })
+ }
+}
+
+impl TryFrom<TabMetadata> for ProtobufTabMetadata {
+ type Error = &'static str;
+ fn try_from(metadata: TabMetadata) -> Result<Self, &'static str> {
+ let pane_metadata = metadata
+ .panes
+ .into_iter()
+ .map(|p| p.try_into())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(ProtobufTabMetadata {
+ pane_metadata,
+ name: metadata.name,
+ })
+ }
+}
+
+impl TryFrom<ProtobufPaneMetadata> for PaneMetadata {
+ type Error = &'static str;
+ fn try_from(protobuf_metadata: ProtobufPaneMetadata) -> Result<Self, &'static str> {
+ Ok(PaneMetadata {
+ name: protobuf_metadata.name,
+ is_plugin: protobuf_metadata.is_plugin,
+ is_builtin_plugin: protobuf_metadata.is_builtin_plugin,
+ })
+ }
+}
+
+impl TryFrom<PaneMetadata> for ProtobufPaneMetadata {
+ type Error = &'static str;
+ fn try_from(metadata: PaneMetadata) -> Result<Self, &'static str> {
+ Ok(ProtobufPaneMetadata {
+ name: metadata.name,
+ is_plugin: metadata.is_plugin,
+ is_builtin_plugin: metadata.is_builtin_plugin,
+ })
+ }
+}
+
+// LayoutWithError conversions
+impl TryFrom<ProtobufLayoutWithError> for crate::data::LayoutWithError {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufLayoutWithError) -> Result<Self, Self::Error> {
+ Ok(crate::data::LayoutWithError {
+ layout_name: protobuf.layout_name,
+ error: protobuf.error.ok_or("Missing error field")?.try_into()?,
+ })
+ }
+}
+
+impl TryFrom<crate::data::LayoutWithError> for ProtobufLayoutWithError {
+ type Error = &'static str;
+ fn try_from(layout_error: crate::data::LayoutWithError) -> Result<Self, Self::Error> {
+ Ok(ProtobufLayoutWithError {
+ layout_name: layout_error.layout_name,
+ error: Some(layout_error.error.try_into()?),
+ })
+ }
+}
+
+// LayoutParsingError conversions
+impl TryFrom<ProtobufLayoutParsingError> for crate::data::LayoutParsingError {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufLayoutParsingError) -> Result<Self, Self::Error> {
+ match protobuf.error_type.ok_or("Missing error_type")? {
+ ProtobufLayoutParsingErrorType::KdlError(kdl_variant) => {
+ Ok(crate::data::LayoutParsingError::KdlError {
+ kdl_error: kdl_variant
+ .kdl_error
+ .ok_or("Missing kdl_error")?
+ .try_into()?,
+ file_name: kdl_variant.file_name,
+ source_code: kdl_variant.source_code,
+ })
+ },
+ ProtobufLayoutParsingErrorType::SyntaxError(_) => {
+ Ok(crate::data::LayoutParsingError::SyntaxError)
+ },
+ }
+ }
+}
+
+impl TryFrom<crate::data::LayoutParsingError> for ProtobufLayoutParsingError {
+ type Error = &'static str;
+ fn try_from(error: crate::data::LayoutParsingError) -> Result<Self, Self::Error> {
+ let error_type = match error {
+ crate::data::LayoutParsingError::KdlError {
+ kdl_error,
+ file_name,
+ source_code,
+ } => ProtobufLayoutParsingErrorType::KdlError(ProtobufKdlErrorVariant {
+ kdl_error: Some(kdl_error.try_into()?),
+ file_name,
+ source_code,
+ }),
+ crate::data::LayoutParsingError::SyntaxError => {
+ ProtobufLayoutParsingErrorType::SyntaxError(ProtobufSyntaxError {})
+ },
+ };
+ Ok(ProtobufLayoutParsingError {
+ error_type: Some(error_type),
+ })
+ }
+}
+
+// KdlError conversions
+impl TryFrom<ProtobufKdlError> for crate::input::config::KdlError {
+ type Error = &'static str;
+ fn try_from(protobuf: ProtobufKdlError) -> Result<Self, Self::Error> {
+ Ok(crate::input::config::KdlError {
+ error_message: protobuf.error_message,
+ src: None, // We don't serialize NamedSource
+ offset: protobuf.offset.map(|o| o as usize),
+ len: protobuf.len.map(|l| l as usize),
+ help_message: protobuf.help_message,
+ })
+ }
+}
+
+impl TryFrom<crate::input::config::KdlError> for ProtobufKdlError {
+ type Error = &'static str;
+ fn try_from(kdl: crate::input::config::KdlError) -> Result<Self, Self::Error> {
+ Ok(ProtobufKdlError {
+ error_message: kdl.error_message,
+ // src is not serialized
+ offset: kdl.offset.map(|o| o as u64),
+ len: kdl.len.map(|l| l as u64),
+ help_message: kdl.help_message,
+ })
+ }
+}
+
impl TryFrom<CopyDestination> for ProtobufCopyDestination {
type Error = &'static str;
fn try_from(copy_destination: CopyDestination) -> Result<Self, &'static str> {
@@ -1620,6 +1862,7 @@ impl TryFrom<ProtobufEventType> for EventType {
ProtobufEventType::UserAction => EventType::UserAction,
ProtobufEventType::ActionComplete => EventType::ActionComplete,
ProtobufEventType::CwdChanged => EventType::CwdChanged,
+ ProtobufEventType::AvailableLayoutInfo => EventType::AvailableLayoutInfo,
})
}
}
@@ -1667,6 +1910,7 @@ impl TryFrom<EventType> for ProtobufEventType {
EventType::UserAction => ProtobufEventType::UserAction,
EventType::ActionComplete => ProtobufEventType::ActionComplete,
EventType::CwdChanged => ProtobufEventType::CwdChanged,
+ EventType::AvailableLayoutInfo => ProtobufEventType::AvailableLayoutInfo,
})
}
}
@@ -2268,9 +2512,23 @@ fn serialize_session_update_event_with_non_default_values() {
connected_clients: 2,
is_current_session: true,
available_layouts: vec![
- LayoutInfo::File("layout 1".to_owned()),
+ LayoutInfo::File(
+ "layout 1".to_owned(),
+ LayoutMetadata {
+ tabs: vec![],
+ creation_time: "0".to_owned(),
+ update_time: "0".to_owned(),
+ },
+ ),
LayoutInfo::BuiltIn("layout2".to_owned()),
- LayoutInfo::File("layout3".to_owned()),
+ LayoutInfo::File(
+ "layout3".to_owned(),
+ LayoutMetadata {
+ tabs: vec![],
+ creation_time: "0".to_owned(),
+ update_time: "0".to_owned(),
+ },
+ ),
],
plugins,
web_clients_allowed: false,
@@ -2287,9 +2545,23 @@ fn serialize_session_update_event_with_non_default_values() {
connected_clients: 0,
is_current_session: false,
available_layouts: vec![
- LayoutInfo::File("layout 1".to_owned()),
+ LayoutInfo::File(
+ "layout 1".to_owned(),
+ LayoutMetadata {
+ tabs: vec![],
+ creation_time: "0".to_owned(),
+ update_time: "0".to_owned(),
+ },
+ ),
LayoutInfo::BuiltIn("layout2".to_owned()),
- LayoutInfo::File("layout3".to_owned()),
+ LayoutInfo::File(
+ "layout3".to_owned(),
+ LayoutMetadata {
+ tabs: vec![],
+ creation_time: "0".to_owned(),
+ update_time: "0".to_owned(),
+ },
+ ),
],
plugins: Default::default(),
web_clients_allowed: false,
diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto
index fb456ccd..0a0ad2b0 100644
--- a/zellij-utils/src/plugin_api/plugin_command.proto
+++ b/zellij-utils/src/plugin_api/plugin_command.proto
@@ -166,6 +166,16 @@ enum CommandName {
SendSigintToPaneId = 171;
SendSigkillToPaneId = 172;
GetPanePid = 173;
+ OverrideLayout = 174;
+ SaveLayout = 175;
+ DeleteLayout = 176;
+ EditLayout = 177;
+ GenerateRandomName = 178;
+ DumpLayout = 179;
+ ParseLayout = 180;
+ RenameLayout = 181;
+ GetLayoutDir = 182;
+ GetFocusedPaneInfo = 183;
}
message PluginCommand {
@@ -280,6 +290,17 @@ message PluginCommand {
PaneId send_sigint_to_pane_id_payload = 121;
PaneId send_sigkill_to_pane_id_payload = 122;
GetPanePidPayload get_pane_pid_payload = 123;
+ OverrideLayoutPayload override_layout_payload = 124;
+ SaveLayoutPayload save_layout_payload = 125;
+ DeleteLayoutPayload delete_layout_payload = 126;
+ EditLayoutPayload edit_layout_payload = 127;
+ GenerateRandomNamePayload generate_random_name_payload = 128;
+ DumpLayoutPayload dump_layout_payload = 129;
+ ParseLayoutPayload parse_layout_payload = 130;
+ RenameLayoutPayload rename_layout_payload = 131;
+ GetLayoutDirPayload get_layout_dir_payload = 132;
+ DumpSessionLayoutPayload dump_session_layout_payload = 133;
+ GetFocusedPaneInfoPayload get_focused_pane_info_payload = 134;
}
}
@@ -288,6 +309,10 @@ message NewTabPayload {
optional string cwd = 2;
}
+message DumpSessionLayoutPayload {
+ optional uint32 tab_index = 1;
+}
+
message RunActionPayload {
action.Action action = 1;
repeated ContextItem context = 2;
@@ -755,3 +780,115 @@ message GetPanePidResponse {
string error = 2;
}
}
+
+message SaveLayoutResponse {
+ oneof result {
+ bool success = 1;
+ string error = 2;
+ }
+}
+
+message DeleteLayoutResponse {
+ oneof result {
+ bool success = 1;
+ string error = 2;
+ }
+}
+
+message EditLayoutResponse {
+ oneof result {
+ bool success = 1;
+ string error = 2;
+ }
+}
+
+message GenerateRandomNameResponse {
+ string name = 1;
+}
+
+message OverrideLayoutPayload {
+ event.LayoutInfo layout_info = 1;
+ repeated ContextItem context = 2;
+ bool retain_existing_terminal_panes = 3;
+ bool retain_existing_plugin_panes = 4;
+ bool apply_only_to_active_tab = 5;
+}
+
+message SaveLayoutPayload {
+ string layout_name = 1;
+ string layout_kdl = 2;
+ bool overwrite = 3;
+}
+
+message DeleteLayoutPayload {
+ string layout_name = 1;
+}
+
+message EditLayoutPayload {
+ string layout_name = 1;
+ repeated ContextItem context = 2;
+}
+
+message GenerateRandomNamePayload {}
+
+message DumpLayoutPayload {
+ string layout_name = 1;
+}
+
+message DumpLayoutResponse {
+ oneof result {
+ string layout_content = 1;
+ string error = 2;
+ }
+}
+
+message DumpSessionLayoutResponse {
+ oneof result {
+ string layout_content = 1;
+ string error = 2;
+ }
+ optional api.event.LayoutMetadata metadata = 3;
+}
+
+message ParseLayoutPayload {
+ string layout_string = 1;
+}
+
+message ParseLayoutResponse {
+ oneof result {
+ api.event.LayoutMetadata metadata = 1;
+ api.event.LayoutParsingError error = 2;
+ }
+}
+
+message RenameLayoutPayload {
+ string old_layout_name = 1;
+ string new_layout_name = 2;
+}
+
+message RenameLayoutResponse {
+ oneof result {
+ bool success = 1;
+ string error = 2;
+ }
+}
+
+message GetLayoutDirPayload {}
+
+message GetLayoutDirResponse {
+ string layout_dir = 1;
+}
+
+message GetFocusedPaneInfoPayload {}
+
+message GetFocusedPaneInfoResponse {
+ oneof result {
+ FocusedPaneInfo focused_pane_info = 1;
+ string error = 2;
+ }
+}
+
+message FocusedPaneInfo {
+ uint32 focused_tab_index = 1;
+ PaneId focused_pane_id = 2;
+}
diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs
index 542e1ea2..8c4bf451 100644
--- a/zellij-utils/src/plugin_api/plugin_command.rs
+++ b/zellij-utils/src/plugin_api/plugin_command.rs
@@ -3,35 +3,48 @@ pub use super::generated_api::api::{
event::{EventNameList as ProtobufEventNameList, Header},
input_mode::InputMode as ProtobufInputMode,
plugin_command::{
- 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,
+ delete_layout_response, dump_layout_response, dump_session_layout_response,
+ edit_layout_response, get_focused_pane_info_response, get_pane_pid_response,
+ parse_layout_response, plugin_command::Payload, rename_layout_response,
+ save_layout_response, BreakPanesToNewTabPayload, BreakPanesToTabWithIndexPayload,
+ ChangeFloatingPanesCoordinatesPayload, ChangeHostFolderPayload,
+ ClearScreenForPaneIdPayload, CliPipeOutputPayload, CloseMultiplePanesPayload,
+ CloseTabWithIndexPayload, CommandName, ContextItem, CopyToClipboardPayload,
+ CreateTokenResponse as ProtobufCreateTokenResponse, CreateTokenResponse, CursorPosition,
+ DeleteLayoutPayload, DeleteLayoutResponse as ProtobufDeleteLayoutResponse,
+ DumpLayoutPayload, DumpLayoutResponse as ProtobufDumpLayoutResponse,
+ DumpSessionLayoutPayload, DumpSessionLayoutResponse as ProtobufDumpSessionLayoutResponse,
+ EditLayoutPayload, EditLayoutResponse as ProtobufEditLayoutResponse,
+ 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,
- MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload,
- MovePayload, NewPluginArgs as ProtobufNewPluginArgs, NewTabPayload,
- NewTabsWithLayoutInfoPayload, OpenCommandPaneFloatingNearPluginPayload,
- OpenCommandPaneInPlaceOfPluginPayload, OpenCommandPaneNearPluginPayload,
- OpenCommandPanePayload, OpenFileFloatingNearPluginPayload, OpenFileInPlaceOfPluginPayload,
+ FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, FocusedPaneInfo,
+ GenerateRandomNamePayload,
+ GenerateRandomNameResponse as ProtobufGenerateRandomNameResponse,
+ GenerateWebLoginTokenPayload, GetFocusedPaneInfoPayload,
+ GetFocusedPaneInfoResponse as ProtobufGetFocusedPaneInfoResponse, GetLayoutDirPayload,
+ GetLayoutDirResponse as ProtobufGetLayoutDirResponse, GetPanePidPayload,
+ GetPanePidResponse as ProtobufGetPanePidResponse, GetPaneScrollbackPayload,
+ GroupAndUngroupPanesPayload, HidePaneWithIdPayload, HighlightAndUnhighlightPanesPayload,
+ HttpVerb as ProtobufHttpVerb, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload,
+ ListTokensResponse, LoadNewPluginPayload, MessageToPluginPayload,
+ MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload, MovePayload,
+ NewPluginArgs as ProtobufNewPluginArgs, NewTabPayload, NewTabsWithLayoutInfoPayload,
+ OpenCommandPaneFloatingNearPluginPayload, OpenCommandPaneInPlaceOfPluginPayload,
+ OpenCommandPaneNearPluginPayload, OpenCommandPanePayload,
+ OpenFileFloatingNearPluginPayload, OpenFileInPlaceOfPluginPayload,
OpenFileNearPluginPayload, OpenFilePayload, OpenTerminalFloatingNearPluginPayload,
- OpenTerminalInPlaceOfPluginPayload, OpenTerminalNearPluginPayload,
+ OpenTerminalInPlaceOfPluginPayload, OpenTerminalNearPluginPayload, OverrideLayoutPayload,
PageScrollDownInPaneIdPayload, PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId,
- PaneIdAndFloatingPaneCoordinates, PaneType as ProtobufPaneType,
- PluginCommand as ProtobufPluginCommand, PluginMessagePayload, RebindKeysPayload,
- ReconfigurePayload, ReloadPluginPayload, RenameWebLoginTokenPayload,
- RenameWebTokenResponse, ReplacePaneWithExistingPanePayload, RequestPluginPermissionPayload,
- RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload,
- RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload,
- RunActionPayload, RunCommandPayload, ScrollDownInPaneIdPayload,
+ PaneIdAndFloatingPaneCoordinates, PaneType as ProtobufPaneType, ParseLayoutPayload,
+ ParseLayoutResponse as ProtobufParseLayoutResponse, PluginCommand as ProtobufPluginCommand,
+ PluginMessagePayload, RebindKeysPayload, ReconfigurePayload, ReloadPluginPayload,
+ RenameLayoutPayload, RenameLayoutResponse as ProtobufRenameLayoutResponse,
+ RenameWebLoginTokenPayload, RenameWebTokenResponse, ReplacePaneWithExistingPanePayload,
+ RequestPluginPermissionPayload, RerunCommandPanePayload, ResizePaneIdWithDirectionPayload,
+ ResizePayload, RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload,
+ RunActionPayload, RunCommandPayload, SaveLayoutPayload,
+ SaveLayoutResponse as ProtobufSaveLayoutResponse, ScrollDownInPaneIdPayload,
ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload,
SetFloatingPanePinnedPayload, SetSelfMouseSelectionSupportPayload, SetTimeoutPayload,
ShowCursorPayload, ShowPaneWithIdPayload, StackPanesPayload, SubscribePayload,
@@ -44,8 +57,10 @@ pub use super::generated_api::api::{
};
use crate::data::{
- ConnectToSession, FloatingPaneCoordinates, GetPanePidResponse, HttpVerb, InputMode,
- KeyWithModifier, MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand,
+ ConnectToSession, DeleteLayoutResponse, EditLayoutResponse, FloatingPaneCoordinates,
+ GetFocusedPaneInfoResponse, GetPanePidResponse, HttpVerb, InputMode, KeyWithModifier,
+ MessageToPlugin, NewPluginArgs, PaneId, PermissionType, PluginCommand, RenameLayoutResponse,
+ SaveLayoutResponse,
};
use crate::input::actions::Action;
use crate::input::layout::SplitSize;
@@ -227,6 +242,135 @@ impl From<GetPanePidResponse> for ProtobufGetPanePidResponse {
}
}
+impl From<GetFocusedPaneInfoResponse> for ProtobufGetFocusedPaneInfoResponse {
+ fn from(response: GetFocusedPaneInfoResponse) -> Self {
+ match response {
+ GetFocusedPaneInfoResponse::Ok { tab_index, pane_id } => {
+ let protobuf_pane_id = ProtobufPaneId {
+ pane_type: match pane_id {
+ PaneId::Terminal(_) => ProtobufPaneType::Terminal as i32,
+ PaneId::Plugin(_) => ProtobufPaneType::Plugin as i32,
+ },
+ id: match pane_id {
+ PaneId::Terminal(id) | PaneId::Plugin(id) => id,
+ },
+ };
+ ProtobufGetFocusedPaneInfoResponse {
+ result: Some(get_focused_pane_info_response::Result::FocusedPaneInfo(
+ FocusedPaneInfo {
+ focused_tab_index: tab_index as u32,
+ focused_pane_id: Some(protobuf_pane_id),
+ },
+ )),
+ }
+ },
+ GetFocusedPaneInfoResponse::Err(err) => ProtobufGetFocusedPaneInfoResponse {
+ result: Some(get_focused_pane_info_response::Result::Error(err)),
+ },
+ }
+ }
+}
+
+impl TryFrom<ProtobufSaveLayoutResponse> for SaveLayoutResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufSaveLayoutResponse) -> Result<Self, &'static str> {
+ match protobuf_response.result {
+ Some(save_layout_response::Result::Success(_)) => Ok(SaveLayoutResponse::Ok(())),
+ Some(save_layout_response::Result::Error(error)) => Ok(SaveLayoutResponse::Err(error)),
+ None => Err("Empty SaveLayoutResponse"),
+ }
+ }
+}
+
+impl From<SaveLayoutResponse> for ProtobufSaveLayoutResponse {
+ fn from(response: SaveLayoutResponse) -> Self {
+ match response {
+ SaveLayoutResponse::Ok(_) => ProtobufSaveLayoutResponse {
+ result: Some(save_layout_response::Result::Success(true)),
+ },
+ SaveLayoutResponse::Err(error) => ProtobufSaveLayoutResponse {
+ result: Some(save_layout_response::Result::Error(error)),
+ },
+ }
+ }
+}
+
+impl TryFrom<ProtobufDeleteLayoutResponse> for DeleteLayoutResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufDeleteLayoutResponse) -> Result<Self, &'static str> {
+ match protobuf_response.result {
+ Some(delete_layout_response::Result::Success(_)) => Ok(DeleteLayoutResponse::Ok(())),
+ Some(delete_layout_response::Result::Error(error)) => {
+ Ok(DeleteLayoutResponse::Err(error))
+ },
+ None => Err("Empty DeleteLayoutResponse"),
+ }
+ }
+}
+
+impl From<DeleteLayoutResponse> for ProtobufDeleteLayoutResponse {
+ fn from(response: DeleteLayoutResponse) -> Self {
+ match response {
+ DeleteLayoutResponse::Ok(_) => ProtobufDeleteLayoutResponse {
+ result: Some(delete_layout_response::Result::Success(true)),
+ },
+ DeleteLayoutResponse::Err(error) => ProtobufDeleteLayoutResponse {
+ result: Some(delete_layout_response::Result::Error(error)),
+ },
+ }
+ }
+}
+
+impl TryFrom<ProtobufRenameLayoutResponse> for RenameLayoutResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufRenameLayoutResponse) -> Result<Self, &'static str> {
+ match protobuf_response.result {
+ Some(rename_layout_response::Result::Success(_)) => Ok(RenameLayoutResponse::Ok(())),
+ Some(rename_layout_response::Result::Error(error)) => {
+ Ok(RenameLayoutResponse::Err(error))
+ },
+ None => Err("Empty RenameLayoutResponse"),
+ }
+ }
+}
+
+impl From<RenameLayoutResponse> for ProtobufRenameLayoutResponse {
+ fn from(response: RenameLayoutResponse) -> Self {
+ match response {
+ RenameLayoutResponse::Ok(_) => ProtobufRenameLayoutResponse {
+ result: Some(rename_layout_response::Result::Success(true)),
+ },
+ RenameLayoutResponse::Err(error) => ProtobufRenameLayoutResponse {
+ result: Some(rename_layout_response::Result::Error(error)),
+ },
+ }
+ }
+}
+
+impl TryFrom<ProtobufEditLayoutResponse> for EditLayoutResponse {
+ type Error = &'static str;
+ fn try_from(protobuf_response: ProtobufEditLayoutResponse) -> Result<Self, &'static str> {
+ match protobuf_response.result {
+ Some(edit_layout_response::Result::Success(_)) => Ok(EditLayoutResponse::Ok(())),
+ Some(edit_layout_response::Result::Error(error)) => Ok(EditLayoutResponse::Err(error)),
+ None => Err("Empty EditLayoutResponse"),
+ }
+ }
+}
+
+impl From<EditLayoutResponse> for ProtobufEditLayoutResponse {
+ fn from(response: EditLayoutResponse) -> Self {
+ match response {
+ EditLayoutResponse::Ok(_) => ProtobufEditLayoutResponse {
+ result: Some(edit_layout_response::Result::Success(true)),
+ },
+ EditLayoutResponse::Err(error) => ProtobufEditLayoutResponse {
+ result: Some(edit_layout_response::Result::Error(error)),
+ },
+ }
+ }
+}
+
impl TryFrom<(InputMode, KeyWithModifier, Vec<Action>)> for KeyToRebind {
type Error = &'static str;
fn try_from(
@@ -1047,8 +1191,13 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
None => Ok(PluginCommand::WatchFilesystem),
},
Some(CommandName::DumpSessionLayout) => match protobuf_plugin_command.payload {
- Some(_) => Err("DumpSessionLayout should have no payload, found a payload"),
- None => Ok(PluginCommand::DumpSessionLayout),
+ Some(Payload::DumpSessionLayoutPayload(payload)) => {
+ Ok(PluginCommand::DumpSessionLayout {
+ tab_index: payload.tab_index.map(|i| i as usize),
+ })
+ },
+ None => Ok(PluginCommand::DumpSessionLayout { tab_index: None }),
+ _ => Err("Mismatched payload for DumpSessionLayout"),
},
Some(CommandName::CloseSelf) => match protobuf_plugin_command.payload {
Some(_) => Err("CloseSelf should have no payload, found a payload"),
@@ -1215,6 +1364,70 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
},
_ => Err("Mismatched payload for GetPanePid"),
},
+ Some(CommandName::OverrideLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::OverrideLayoutPayload(override_layout_payload)) => {
+ let layout_info = override_layout_payload
+ .layout_info
+ .ok_or("OverrideLayout missing layout_info")?
+ .try_into()
+ .map_err(|_| "Failed to parse LayoutInfo")?;
+ let context = override_layout_payload
+ .context
+ .into_iter()
+ .map(|c| (c.name, c.value))
+ .collect();
+ Ok(PluginCommand::OverrideLayout(
+ layout_info,
+ override_layout_payload.retain_existing_terminal_panes,
+ override_layout_payload.retain_existing_plugin_panes,
+ override_layout_payload.apply_only_to_active_tab,
+ context,
+ ))
+ },
+ _ => Err("Mismatched payload for OverrideLayout"),
+ },
+ Some(CommandName::SaveLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::SaveLayoutPayload(save_layout_payload)) => {
+ Ok(PluginCommand::SaveLayout {
+ layout_name: save_layout_payload.layout_name,
+ layout_kdl: save_layout_payload.layout_kdl,
+ overwrite: save_layout_payload.overwrite,
+ })
+ },
+ _ => Err("Mismatched payload for SaveLayout"),
+ },
+ Some(CommandName::DeleteLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::DeleteLayoutPayload(delete_layout_payload)) => {
+ Ok(PluginCommand::DeleteLayout {
+ layout_name: delete_layout_payload.layout_name,
+ })
+ },
+ _ => Err("Mismatched payload for DeleteLayout"),
+ },
+ Some(CommandName::RenameLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::RenameLayoutPayload(rename_layout_payload)) => {
+ Ok(PluginCommand::RenameLayout {
+ old_layout_name: rename_layout_payload.old_layout_name,
+ new_layout_name: rename_layout_payload.new_layout_name,
+ })
+ },
+ _ => Err("Mismatched payload for RenameLayout"),
+ },
+ Some(CommandName::EditLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::EditLayoutPayload(edit_layout_payload)) => {
+ let layout_name = edit_layout_payload.layout_name;
+ let context: BTreeMap<String, String> = edit_layout_payload
+ .context
+ .into_iter()
+ .map(|e| (e.name, e.value))
+ .collect();
+ Ok(PluginCommand::EditLayout {
+ layout_name,
+ context,
+ })
+ },
+ _ => Err("Mismatched payload for EditLayout"),
+ },
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 {
@@ -1845,6 +2058,21 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
},
_ => Err("Mismatched payload for CopyToClipboard"),
},
+ Some(CommandName::GenerateRandomName) => Ok(PluginCommand::GenerateRandomName),
+ Some(CommandName::DumpLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::DumpLayoutPayload(payload)) => {
+ Ok(PluginCommand::DumpLayout(payload.layout_name))
+ },
+ _ => Err("Mismatched payload for DumpLayout"),
+ },
+ Some(CommandName::ParseLayout) => match protobuf_plugin_command.payload {
+ Some(Payload::ParseLayoutPayload(payload)) => {
+ Ok(PluginCommand::ParseLayout(payload.layout_string))
+ },
+ _ => Err("Mismatched payload for ParseLayout"),
+ },
+ Some(CommandName::GetLayoutDir) => Ok(PluginCommand::GetLayoutDir),
+ Some(CommandName::GetFocusedPaneInfo) => Ok(PluginCommand::GetFocusedPaneInfo),
None => Err("Unrecognized plugin command"),
}
}
@@ -2417,9 +2645,13 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
name: CommandName::WatchFilesystem as i32,
payload: None,
}),
- PluginCommand::DumpSessionLayout => Ok(ProtobufPluginCommand {
+ PluginCommand::DumpSessionLayout { tab_index } => Ok(ProtobufPluginCommand {
name: CommandName::DumpSessionLayout as i32,
- payload: None,
+ payload: tab_index.map(|idx| {
+ Payload::DumpSessionLayoutPayload(DumpSessionLayoutPayload {
+ tab_index: Some(idx as u32),
+ })
+ }),
}),
PluginCommand::CloseSelf => Ok(ProtobufPluginCommand {
name: CommandName::CloseSelf as i32,
@@ -2545,6 +2777,69 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
pane_id: Some(pane_id.try_into()?),
})),
}),
+ PluginCommand::OverrideLayout(
+ layout_info,
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ apply_only_to_active_tab,
+ context,
+ ) => Ok(ProtobufPluginCommand {
+ name: CommandName::OverrideLayout as i32,
+ payload: Some(Payload::OverrideLayoutPayload(OverrideLayoutPayload {
+ layout_info: layout_info.try_into().ok(),
+ context: context
+ .into_iter()
+ .map(|(name, value)| ContextItem { name, value })
+ .collect(),
+ retain_existing_terminal_panes,
+ retain_existing_plugin_panes,
+ apply_only_to_active_tab,
+ })),
+ }),
+ PluginCommand::SaveLayout {
+ layout_name,
+ layout_kdl,
+ overwrite,
+ } => Ok(ProtobufPluginCommand {
+ name: CommandName::SaveLayout as i32,
+ payload: Some(Payload::SaveLayoutPayload(SaveLayoutPayload {
+ layout_name,
+ layout_kdl,
+ overwrite,
+ })),
+ }),
+ PluginCommand::DeleteLayout { layout_name } => Ok(ProtobufPluginCommand {
+ name: CommandName::DeleteLayout as i32,
+ payload: Some(Payload::DeleteLayoutPayload(DeleteLayoutPayload {
+ layout_name,
+ })),
+ }),
+ PluginCommand::RenameLayout {
+ old_layout_name,
+ new_layout_name,
+ } => Ok(ProtobufPluginCommand {
+ name: CommandName::RenameLayout as i32,
+ payload: Some(Payload::RenameLayoutPayload(RenameLayoutPayload {
+ old_layout_name,
+ new_layout_name,
+ })),
+ }),
+ PluginCommand::EditLayout {
+ layout_name,
+ context,
+ } => {
+ let context: Vec<_> = context
+ .into_iter()
+ .map(|(name, value)| ContextItem { name, value })
+ .collect();
+ Ok(ProtobufPluginCommand {
+ name: CommandName::EditLayout as i32,
+ payload: Some(Payload::EditLayoutPayload(EditLayoutPayload {
+ layout_name,
+ context,
+ })),
+ })
+ },
PluginCommand::MovePaneWithPaneId(pane_id) => Ok(ProtobufPluginCommand {
name: CommandName::MovePaneWithPaneId as i32,
payload: Some(Payload::MovePaneWithPaneIdPayload(
@@ -3073,6 +3368,34 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
text,
})),
}),
+ PluginCommand::GenerateRandomName => Ok(ProtobufPluginCommand {
+ name: CommandName::GenerateRandomName as i32,
+ payload: Some(Payload::GenerateRandomNamePayload(
+ GenerateRandomNamePayload {},
+ )),
+ }),
+ PluginCommand::DumpLayout(layout_name) => Ok(ProtobufPluginCommand {
+ name: CommandName::DumpLayout as i32,
+ payload: Some(Payload::DumpLayoutPayload(DumpLayoutPayload {
+ layout_name,
+ })),
+ }),
+ PluginCommand::ParseLayout(layout_string) => Ok(ProtobufPluginCommand {
+ name: CommandName::ParseLayout as i32,
+ payload: Some(Payload::ParseLayoutPayload(ParseLayoutPayload {
+ layout_string,
+ })),
+ }),
+ PluginCommand::GetLayoutDir => Ok(ProtobufPluginCommand {
+ name: CommandName::GetLayoutDir as i32,
+ payload: Some(Payload::GetLayoutDirPayload(GetLayoutDirPayload {})),
+ }),
+ PluginCommand::GetFocusedPaneInfo => Ok(ProtobufPluginCommand {
+ name: CommandName::GetFocusedPaneInfo as i32,
+ payload: Some(Payload::GetFocusedPaneInfoPayload(
+ GetFocusedPaneInfoPayload {},
+ )),
+ }),
}
}
}
diff --git a/zellij-utils/src/sessions.rs b/zellij-utils/src/sessions.rs
index b72be064..57d79779 100644
--- a/zellij-utils/src/sessions.rs
+++ b/zellij-utils/src/sessions.rs
@@ -493,6 +493,12 @@ pub fn get_name_generator() -> impl Iterator<Item = String> {
names::Generator::new(&ADJECTIVES, &NOUNS, names::Name::Plain)
}
+/// Generates a random human-readable name using curated adjectives and nouns.
+/// Returns a single name in the format: AdjectiveNoun (e.g., "BraveRustacean")
+pub fn generate_random_name() -> String {
+ get_name_generator().next().unwrap()
+}
+
const ADJECTIVES: &[&'static str] = &[
"adamant",
"adept",
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 3e60c317..9635303b 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
@@ -4326,6 +4326,36 @@ Config {
bare_key: Char(
'l',
),
+ key_modifiers: {},
+ }: [
+ LaunchOrFocusPlugin {
+ plugin: RunPlugin(
+ RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: Zellij(
+ PluginTag(
+ "layout-manager",
+ ),
+ ),
+ configuration: PluginUserConfiguration(
+ {},
+ ),
+ initial_cwd: None,
+ },
+ ),
+ should_float: true,
+ move_to_focused_tab: true,
+ should_open_in_place: false,
+ skip_cache: false,
+ },
+ SwitchToMode {
+ input_mode: Normal,
+ },
+ ],
+ KeyWithModifier {
+ bare_key: Char(
+ 'l',
+ ),
key_modifiers: {
Alt,
},
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 e0fa22f9..879be0ef 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
@@ -4326,6 +4326,36 @@ Config {
bare_key: Char(
'l',
),
+ key_modifiers: {},
+ }: [
+ LaunchOrFocusPlugin {
+ plugin: RunPlugin(
+ RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: Zellij(
+ PluginTag(
+ "layout-manager",
+ ),
+ ),
+ configuration: PluginUserConfiguration(
+ {},
+ ),
+ initial_cwd: None,
+ },
+ ),
+ should_float: true,
+ move_to_focused_tab: true,
+ should_open_in_place: false,
+ skip_cache: false,
+ },
+ SwitchToMode {
+ input_mode: Normal,
+ },
+ ],
+ KeyWithModifier {
+ bare_key: Char(
+ 'l',
+ ),
key_modifiers: {
Alt,
},
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 e9935ef3..bc5119f7 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
@@ -4326,6 +4326,36 @@ Config {
bare_key: Char(
'l',
),
+ key_modifiers: {},
+ }: [
+ LaunchOrFocusPlugin {
+ plugin: RunPlugin(
+ RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: Zellij(
+ PluginTag(
+ "layout-manager",
+ ),
+ ),
+ configuration: PluginUserConfiguration(
+ {},
+ ),
+ initial_cwd: None,
+ },
+ ),
+ should_float: true,
+ move_to_focused_tab: true,
+ should_open_in_place: false,
+ skip_cache: false,
+ },
+ SwitchToMode {
+ input_mode: Normal,
+ },
+ ],
+ KeyWithModifier {
+ bare_key: Char(
+ 'l',
+ ),
key_modifiers: {
Alt,
},
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 368b5d88..477e152b 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
@@ -4326,6 +4326,36 @@ Config {
bare_key: Char(
'l',
),
+ key_modifiers: {},
+ }: [
+ LaunchOrFocusPlugin {
+ plugin: RunPlugin(
+ RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: Zellij(
+ PluginTag(
+ "layout-manager",
+ ),
+ ),
+ configuration: PluginUserConfiguration(
+ {},
+ ),
+ initial_cwd: None,
+ },
+ ),
+ should_float: true,
+ move_to_focused_tab: true,
+ should_open_in_place: false,
+ skip_cache: false,
+ },
+ SwitchToMode {
+ input_mode: Normal,
+ },
+ ],
+ KeyWithModifier {
+ bare_key: Char(
+ 'l',
+ ),
key_modifiers: {
Alt,
},