diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-01-13 11:27:07 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-13 11:27:07 +0100 |
| commit | 4a27ceea0d99b036ca607e05714cef15689d7c26 (patch) | |
| tree | 22c0148718a82082bdb196c5e867697f514cc541 /zellij-utils/src | |
| parent | docs(CONTRIBUTING): Describe rust toolchain versioning policy (#4585) (diff) | |
| download | sl0ppij-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')
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, }, |
