From 01e21e18ff53190f88c4d27887c339330b61a8ff Mon Sep 17 00:00:00 2001 From: alekspickle Date: Wed, 12 Apr 2023 20:52:31 +0200 Subject: [PATCH 001/128] add necessary actions in server and utils --- zellij-server/src/route.rs | 6 ++++++ zellij-server/src/screen.rs | 14 ++++++++++++++ zellij-utils/src/cli.rs | 5 +++++ zellij-utils/src/errors.rs | 1 + zellij-utils/src/input/actions.rs | 5 +++++ zellij-utils/src/setup.rs | 5 +---- 6 files changed, 32 insertions(+), 4 deletions(-) diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 53b43ecdc0..e0a92b5db3 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -178,6 +178,12 @@ pub(crate) fn route_action( .send_to_screen(ScreenInstruction::DumpScreen(val, client_id, full)) .with_context(err_context)?; }, + Action::DumpLayout(layout) => { + session + .senders + .send_to_screen(ScreenInstruction::DumpLayout(client_id, layout)) + .with_context(err_context)?; + }, Action::EditScrollback => { session .senders diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index a2fa3e5066..7cd37be2f1 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -158,6 +158,7 @@ pub enum ScreenInstruction { MovePaneLeft(ClientId), Exit, DumpScreen(String, ClientId, bool), + DumpLayout(ClientId, Option), EditScrollback(ClientId), ScrollUp(ClientId), ScrollUpAt(Position, ClientId), @@ -312,6 +313,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::MovePaneLeft(..) => ScreenContext::MovePaneLeft, ScreenInstruction::Exit => ScreenContext::Exit, ScreenInstruction::DumpScreen(..) => ScreenContext::DumpScreen, + ScreenInstruction::DumpLayout(..) => ScreenContext::DumpLayout, ScreenInstruction::EditScrollback(..) => ScreenContext::EditScrollback, ScreenInstruction::ScrollUp(..) => ScreenContext::ScrollUp, ScreenInstruction::ScrollDown(..) => ScreenContext::ScrollDown, @@ -2067,6 +2069,18 @@ pub(crate) fn screen_thread_main( screen.unblock_input()?; screen.render()?; }, + ScreenInstruction::DumpLayout(client_id, layout) => { + let layout_ids: Vec<(Tab, Vec)> = screen + .tabs + .values() + .cloned() + .map(|t| (t.1, t.get_all_pane_ids())) + .collect(); + let pane_ids = tab.get_all_pane_ids(); + println!("TABS: {:?}", clients); + screen.unblock_input()?; + screen.render()?; + }, ScreenInstruction::GoToTab(tab_index, client_id) => { let client_id_to_switch = if client_id.is_none() { None diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 3947dfad39..285675c750 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -220,6 +220,11 @@ pub enum CliAction { #[clap(short, long, value_parser, default_value("false"), takes_value(false))] full: bool, }, + /// Dump current layout to a specified or default layout directory, + /// and in case path is not provided - stdout + DumpLayout { + path: Option, + }, /// Open the pane scrollback in your default editor EditScrollback, /// Scroll up in the focused pane diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index f1522c1de9..d8ca6b8276 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -256,6 +256,7 @@ pub enum ScreenContext { MovePaneLeft, Exit, DumpScreen, + DumpLayout, EditScrollback, ScrollUp, ScrollUpAt, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index b2ec8d8287..d851edc7aa 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -121,6 +121,8 @@ pub enum Action { MovePaneBackwards, /// Dumps the screen to a file DumpScreen(String, bool), + /// Dumps + DumpLayout(Option), /// Scroll up in focus pane. EditScrollback, ScrollUp, @@ -255,6 +257,9 @@ impl Action { path.as_os_str().to_string_lossy().into(), full, )]), + CliAction::DumpLayout { path } => Ok(vec![Action::DumpLayout( + path.map(|p| p.as_os_str().to_string_lossy().into()), + )]), CliAction::EditScrollback => Ok(vec![Action::EditScrollback]), CliAction::ScrollUp => Ok(vec![Action::ScrollUp]), CliAction::ScrollDown => Ok(vec![Action::ScrollDown]), diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 0086c6a992..409699839a 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -186,10 +186,7 @@ pub fn dump_specified_layout(layout: &str) -> std::io::Result<()> { "default" => dump_asset(DEFAULT_LAYOUT), "compact" => dump_asset(COMPACT_BAR_LAYOUT), "disable-status" => dump_asset(NO_STATUS_LAYOUT), - not_found => Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Layout: {} not found", not_found), - )), + current => todo!(), } } From ad4480c3a22dfd78432978d1796a7619868c2773 Mon Sep 17 00:00:00 2001 From: alekspickle Date: Tue, 16 May 2023 16:03:39 +0200 Subject: [PATCH 002/128] update --- docs/MANPAGE.md | 1 + src/commands.rs | 3 +++ zellij-server/src/screen.rs | 23 +++++++++++------------ zellij-utils/src/kdl/mod.rs | 6 ++++++ zellij-utils/src/setup.rs | 8 ++++++-- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/docs/MANPAGE.md b/docs/MANPAGE.md index cd662ef4cf..f3fa8e1802 100644 --- a/docs/MANPAGE.md +++ b/docs/MANPAGE.md @@ -154,6 +154,7 @@ ACTIONS * __MoveFocus: __ - moves focus in the specified direction (Left, Right, Up, Down). * __DumpScreen: __ - dumps the screen in the specified file. +* __DumpLayout: __ - dumps the screen in the specified or default file. * __EditScrollback__ - replaces the current pane with the scrollback buffer. * __ScrollUp__ - scrolls up 1 line in the focused pane. * __ScrollDown__ - scrolls down 1 line in the focused pane. diff --git a/src/commands.rs b/src/commands.rs index 0394a93428..04bbae5618 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -116,6 +116,9 @@ fn find_indexed_session( } } +/// Client entrypoint for all [`zellij_utils::cli::CliAction`] +/// +/// Checks session to send the action to and attaches with client pub(crate) fn send_action_to_session( cli_action: zellij_utils::cli::CliAction, requested_session_name: Option, diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 7cd37be2f1..960b3d21db 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -1716,6 +1716,17 @@ pub(crate) fn screen_thread_main( screen.render()?; screen.unblock_input()?; }, + ScreenInstruction::DumpLayout(client_id, layout) => { + let layout: Vec<(&Tab, Vec)> = screen + .tabs + .values() + .map(|t| (t, t.get_all_pane_ids())) + .collect(); + let tab_names: Vec = layout.iter().map(|(t, _)| t.name.clone()).collect(); + println!("TABS: {:?}", tab_names); + screen.unblock_input()?; + screen.render()?; + }, ScreenInstruction::EditScrollback(client_id) => { active_tab_and_connected_client_id!( screen, @@ -2069,18 +2080,6 @@ pub(crate) fn screen_thread_main( screen.unblock_input()?; screen.render()?; }, - ScreenInstruction::DumpLayout(client_id, layout) => { - let layout_ids: Vec<(Tab, Vec)> = screen - .tabs - .values() - .cloned() - .map(|t| (t.1, t.get_all_pane_ids())) - .collect(); - let pane_ids = tab.get_all_pane_ids(); - println!("TABS: {:?}", clients); - screen.unblock_input()?; - screen.render()?; - }, ScreenInstruction::GoToTab(tab_index, client_id) => { let client_id_to_switch = if client_id.is_none() { None diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 64fe0bac47..1fb81d7213 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -456,6 +456,7 @@ impl Action { }, "MovePaneBackwards" => Ok(Action::MovePaneBackwards), "DumpScreen" => Ok(Action::DumpScreen(string, false)), + "DumpLayout" => Ok(Action::DumpLayout(Some(string))), "NewPane" => { if string.is_empty() { return Ok(Action::NewPane(None, None)); @@ -742,6 +743,11 @@ impl TryFrom<(&KdlNode, &Options)> for Action { action_arguments, kdl_action ), + "DumpLayout" => parse_kdl_action_char_or_string_arguments!( + action_name, + action_arguments, + kdl_action + ), "NewPane" => parse_kdl_action_char_or_string_arguments!( action_name, action_arguments, diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 409699839a..e27599f8a5 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -186,6 +186,10 @@ pub fn dump_specified_layout(layout: &str) -> std::io::Result<()> { "default" => dump_asset(DEFAULT_LAYOUT), "compact" => dump_asset(COMPACT_BAR_LAYOUT), "disable-status" => dump_asset(NO_STATUS_LAYOUT), + "current" => { + log::info!("DUMP CURRENT"); + Ok(()) + }, current => todo!(), } } @@ -253,7 +257,7 @@ pub struct Setup { #[clap(long, value_parser)] pub check: bool, - /// Dump the specified layout file to stdout + /// Dump specified layout to stdout #[clap(long, value_parser)] pub dump_layout: Option, @@ -364,7 +368,7 @@ impl Setup { } if let Some(layout) = &self.dump_layout { - dump_specified_layout(layout)?; + dump_specified_layout(&layout)?; std::process::exit(0); } From a69604ebbc2cbc9b381c36e82db1f3dc030dd7d4 Mon Sep 17 00:00:00 2001 From: alekspickle Date: Wed, 28 Jun 2023 19:33:38 +0200 Subject: [PATCH 003/128] move all logic relevant to local default config directories to utils::home --- .../convert_old_yaml_files.rs | 2 +- zellij-server/src/lib.rs | 2 +- zellij-server/src/route.rs | 3 +- zellij-utils/src/home.rs | 109 ++++++++++++++++++ zellij-utils/src/input/actions.rs | 2 +- zellij-utils/src/input/config.rs | 4 +- zellij-utils/src/input/layout.rs | 13 ++- zellij-utils/src/kdl/mod.rs | 3 +- zellij-utils/src/lib.rs | 2 + zellij-utils/src/setup.rs | 98 +++------------- 10 files changed, 144 insertions(+), 94 deletions(-) create mode 100644 zellij-utils/src/home.rs diff --git a/zellij-client/src/old_config_converter/convert_old_yaml_files.rs b/zellij-client/src/old_config_converter/convert_old_yaml_files.rs index 346b6eb62a..dde7308914 100644 --- a/zellij-client/src/old_config_converter/convert_old_yaml_files.rs +++ b/zellij-client/src/old_config_converter/convert_old_yaml_files.rs @@ -2,7 +2,7 @@ use super::{config_yaml_to_config_kdl, layout_yaml_to_layout_kdl}; use std::path::PathBuf; use zellij_utils::{ cli::CliArgs, - setup::{find_default_config_dir, get_layout_dir, get_theme_dir}, + home::{find_default_config_dir, get_layout_dir, get_theme_dir}, }; const OLD_CONFIG_NAME: &str = "config.yaml"; diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 53a08f6565..34b14ff580 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -43,6 +43,7 @@ use zellij_utils::{ consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE}, data::{Event, PluginCapabilities}, errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext}, + home::get_default_data_dir, input::{ command::{RunCommand, TerminalAction}, get_mode_info, @@ -51,7 +52,6 @@ use zellij_utils::{ plugins::PluginsConfig, }, ipc::{ClientAttributes, ExitReason, ServerToClientMsg}, - setup::get_default_data_dir, }; pub type ClientId = u16; diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 98ac14724a..5702bf86d2 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -173,8 +173,7 @@ pub(crate) fn route_action( .with_context(err_context)?; }, Action::DumpLayout(layout) => { - session - .senders + senders .send_to_screen(ScreenInstruction::DumpLayout(client_id, layout)) .with_context(err_context)?; }, diff --git a/zellij-utils/src/home.rs b/zellij-utils/src/home.rs new file mode 100644 index 0000000000..8e7190ba27 --- /dev/null +++ b/zellij-utils/src/home.rs @@ -0,0 +1,109 @@ +//! +//! # This module contain everything you'll need to access local system paths +//! containing configuration and layouts + +use crate::input::theme::Themes; +use crate::{ + consts::{ + SYSTEM_DEFAULT_CONFIG_DIR, SYSTEM_DEFAULT_DATA_DIR_PREFIX, ZELLIJ_DEFAULT_THEMES, + ZELLIJ_PROJ_DIR, + }, + errors::prelude::*, + input::{ + config::{Config, ConfigError}, + layout::Layout, + options::Options, + }, +}; +use clap::{Args, IntoApp}; +use clap_complete::Shell; +use directories_next::BaseDirs; +use log::info; +use serde::{Deserialize, Serialize}; +use std::{ + convert::TryFrom, fmt::Write as FmtWrite, io::Write, path::Path, path::PathBuf, process, +}; + +pub(crate) const CONFIG_LOCATION: &str = ".config/zellij"; + +#[cfg(not(test))] +/// Goes through a predefined list and checks for an already +/// existing config directory, returns the first match +pub fn find_default_config_dir() -> Option { + default_config_dirs() + .into_iter() + .filter(|p| p.is_some()) + .find(|p| p.clone().unwrap().exists()) + .flatten() +} + +#[cfg(test)] +pub fn find_default_config_dir() -> Option { + None +} + +/// Order in which config directories are checked +pub(crate) fn default_config_dirs() -> Vec> { + vec![ + home_config_dir(), + Some(xdg_config_dir()), + Some(Path::new(SYSTEM_DEFAULT_CONFIG_DIR).to_path_buf()), + ] +} + +/// Looks for an existing dir, uses that, else returns a +/// dir matching the config spec. +pub fn get_default_data_dir() -> PathBuf { + [ + xdg_data_dir(), + Path::new(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij"), + ] + .into_iter() + .find(|p| p.exists()) + .unwrap_or_else(xdg_data_dir) +} + +#[cfg(not(test))] +pub(crate) fn get_default_themes() -> Themes { + let mut themes = Themes::default(); + for file in ZELLIJ_DEFAULT_THEMES.files() { + if let Some(content) = file.contents_utf8() { + match Themes::from_string(content.to_string()) { + Ok(theme) => themes = themes.merge(theme), + Err(_) => {}, + } + } + } + + themes +} + +#[cfg(test)] +pub(crate) fn get_default_themes() -> Themes { + Themes::default() +} + +pub fn xdg_config_dir() -> PathBuf { + ZELLIJ_PROJ_DIR.config_dir().to_owned() +} + +pub fn xdg_data_dir() -> PathBuf { + ZELLIJ_PROJ_DIR.data_dir().to_owned() +} + +pub fn home_config_dir() -> Option { + if let Some(user_dirs) = BaseDirs::new() { + let config_dir = user_dirs.home_dir().join(CONFIG_LOCATION); + Some(config_dir) + } else { + None + } +} + +pub fn get_layout_dir(config_dir: Option) -> Option { + config_dir.map(|dir| dir.join("layouts")) +} + +pub fn get_theme_dir(config_dir: Option) -> Option { + config_dir.map(|dir| dir.join("themes")) +} diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index aa5ac364da..d4f69652e2 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -8,9 +8,9 @@ use super::layout::{ use crate::cli::CliAction; use crate::data::InputMode; use crate::data::{Direction, Resize}; +use crate::home::{find_default_config_dir, get_layout_dir}; use crate::input::config::{Config, ConfigError, KdlError}; use crate::input::options::OnForceClose; -use crate::setup::{find_default_config_dir, get_layout_dir}; use miette::{NamedSource, Report}; use serde::{Deserialize, Serialize}; diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index 4ad4ef89d6..a6816ff43d 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -13,7 +13,7 @@ use super::plugins::{PluginsConfig, PluginsConfigError}; use super::theme::{Themes, UiConfig}; use crate::cli::{CliArgs, Command}; use crate::envs::EnvironmentVariables; -use crate::setup; +use crate::{setup, home}; const DEFAULT_CONFIG_FILE_NAME: &str = "config.kdl"; @@ -143,7 +143,7 @@ impl TryFrom<&CliArgs> for Config { let config_dir = opts .config_dir .clone() - .or_else(setup::find_default_config_dir); + .or_else(home::find_default_config_dir); if let Some(ref config) = config_dir { let path = config.join(DEFAULT_CONFIG_FILE_NAME); diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index c1d95a125a..01247a8788 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -10,12 +10,13 @@ // then [`zellij-utils`] could be a proper place. use crate::{ data::Direction, + home::find_default_config_dir, input::{ command::RunCommand, config::{Config, ConfigError}, }, pane_size::{Dimension, PaneGeom}, - setup, + setup::{self}, }; use std::str::FromStr; @@ -729,7 +730,15 @@ impl Layout { Layout::stringified_from_default_assets(layout) } }, - None => Layout::stringified_from_default_assets(layout), + None => { + let home = find_default_config_dir(); + let Some(home) = home else { + return Layout::stringified_from_default_assets(layout) + }; + + let layout_path = &home.join(layout); + Self::stringified_from_path(layout_path) + }, } } pub fn stringified_from_path( diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index f7003cf73c..b05a677de4 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -1,13 +1,14 @@ mod kdl_layout_parser; + use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, Resize}; 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, RunPlugin, RunPluginLocation}; use crate::input::options::{Clipboard, OnForceClose, Options}; use crate::input::plugins::{PluginConfig, PluginTag, PluginType, PluginsConfig}; use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig}; -use crate::setup::{find_default_config_dir, get_layout_dir}; use kdl_layout_parser::KdlLayoutParser; use std::collections::HashMap; use strum::IntoEnumIterator; diff --git a/zellij-utils/src/lib.rs b/zellij-utils/src/lib.rs index b07fe8bdc2..dd314a8992 100644 --- a/zellij-utils/src/lib.rs +++ b/zellij-utils/src/lib.rs @@ -6,9 +6,11 @@ pub mod errors; pub mod input; pub mod kdl; pub mod pane_size; +pub mod persistence; pub mod position; pub mod setup; pub mod shared; +pub mod home; // The following modules can't be used when targeting wasm #[cfg(not(target_family = "wasm"))] diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 4e2c0c737d..88628b97b6 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -8,6 +8,7 @@ use crate::{ ZELLIJ_DEFAULT_THEMES, ZELLIJ_PROJ_DIR, }, errors::prelude::*, + home::*, input::{ config::{Config, ConfigError}, layout::Layout, @@ -17,96 +18,15 @@ use crate::{ use clap::{Args, IntoApp}; use clap_complete::Shell; use directories_next::BaseDirs; +use log::info; use serde::{Deserialize, Serialize}; use std::{ convert::TryFrom, fmt::Write as FmtWrite, io::Write, path::Path, path::PathBuf, process, }; -const CONFIG_LOCATION: &str = ".config/zellij"; const CONFIG_NAME: &str = "config.kdl"; static ARROW_SEPARATOR: &str = ""; -#[cfg(not(test))] -/// Goes through a predefined list and checks for an already -/// existing config directory, returns the first match -pub fn find_default_config_dir() -> Option { - default_config_dirs() - .into_iter() - .filter(|p| p.is_some()) - .find(|p| p.clone().unwrap().exists()) - .flatten() -} - -#[cfg(test)] -pub fn find_default_config_dir() -> Option { - None -} - -/// Order in which config directories are checked -fn default_config_dirs() -> Vec> { - vec![ - home_config_dir(), - Some(xdg_config_dir()), - Some(Path::new(SYSTEM_DEFAULT_CONFIG_DIR).to_path_buf()), - ] -} - -/// Looks for an existing dir, uses that, else returns a -/// dir matching the config spec. -pub fn get_default_data_dir() -> PathBuf { - [ - xdg_data_dir(), - Path::new(SYSTEM_DEFAULT_DATA_DIR_PREFIX).join("share/zellij"), - ] - .into_iter() - .find(|p| p.exists()) - .unwrap_or_else(xdg_data_dir) -} - -#[cfg(not(test))] -fn get_default_themes() -> Themes { - let mut themes = Themes::default(); - for file in ZELLIJ_DEFAULT_THEMES.files() { - if let Some(content) = file.contents_utf8() { - match Themes::from_string(content.to_string()) { - Ok(theme) => themes = themes.merge(theme), - Err(_) => {}, - } - } - } - - themes -} - -#[cfg(test)] -fn get_default_themes() -> Themes { - Themes::default() -} - -pub fn xdg_config_dir() -> PathBuf { - ZELLIJ_PROJ_DIR.config_dir().to_owned() -} - -pub fn xdg_data_dir() -> PathBuf { - ZELLIJ_PROJ_DIR.data_dir().to_owned() -} - -pub fn home_config_dir() -> Option { - if let Some(user_dirs) = BaseDirs::new() { - let config_dir = user_dirs.home_dir().join(CONFIG_LOCATION); - Some(config_dir) - } else { - None - } -} - -pub fn get_layout_dir(config_dir: Option) -> Option { - config_dir.map(|dir| dir.join("layouts")) -} - -pub fn get_theme_dir(config_dir: Option) -> Option { - config_dir.map(|dir| dir.join("themes")) -} pub fn dump_asset(asset: &[u8]) -> std::io::Result<()> { std::io::stdout().write_all(asset)?; Ok(()) @@ -207,10 +127,20 @@ pub fn dump_specified_layout(layout: &str) -> std::io::Result<()> { "compact" => dump_asset(COMPACT_BAR_LAYOUT), "disable-status" => dump_asset(NO_STATUS_LAYOUT), "current" => { - log::info!("DUMP CURRENT"); + info!("DUMP CURRENT"); + Ok(()) + }, + custom => { + info!("Dump {custom} layout"); + let dir = find_default_config_dir(); + let path = Some(PathBuf::from(custom)); + let Ok((layout_path, ..)) = Layout::stringified_from_path_or_default(path.as_ref(), dir) else { + log::error!("No layout named {custom} found"); + return Ok(()) + }; + std::io::stdout().write_all(layout_path.as_bytes())?; Ok(()) }, - current => todo!(), } } From 65da17309ae0d25aa55e0f5a409862cc0552745a Mon Sep 17 00:00:00 2001 From: alekspickle Date: Wed, 28 Jun 2023 20:16:55 +0200 Subject: [PATCH 004/128] add debug statements for pane geom --- zellij-server/src/screen.rs | 31 ++-- zellij-server/src/tab/mod.rs | 2 +- zellij-utils/src/home.rs | 10 +- zellij-utils/src/pane_size.rs | 32 +++- zellij-utils/src/persistence.rs | 257 ++++++++++++++++++++++++++++++++ zellij-utils/src/setup.rs | 21 ++- 6 files changed, 332 insertions(+), 21 deletions(-) create mode 100644 zellij-utils/src/persistence.rs diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 1c3158d944..5305704765 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -10,13 +10,14 @@ use zellij_utils::data::{Direction, PaneManifest, Resize, ResizeStrategy}; use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; use zellij_utils::input::options::Clipboard; -use zellij_utils::pane_size::{Size, SizeInPixels}; +use zellij_utils::pane_size::{PaneGeom, Size, SizeInPixels}; use zellij_utils::{ input::command::TerminalAction, input::layout::{ FloatingPaneLayout, Run, RunPlugin, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }, + persistence, position::Position, }; @@ -29,7 +30,7 @@ use crate::{ panes::PaneId, plugins::PluginInstruction, pty::{ClientOrTabIndex, PtyInstruction, VteBytes}, - tab::Tab, + tab::{Pane, Tab}, thread_bus::Bus, ui::{ loading_indication::LoadingIndication, @@ -1928,13 +1929,25 @@ pub(crate) fn screen_thread_main( screen.unblock_input()?; }, ScreenInstruction::DumpLayout(client_id, layout) => { - let layout: Vec<(&Tab, Vec)> = screen - .tabs - .values() - .map(|t| (t, t.get_all_pane_ids())) - .collect(); - let tab_names: Vec = layout.iter().map(|(t, _)| t.name.clone()).collect(); - println!("TABS: {:?}", tab_names); + for tab in screen.tabs.values() { + log::warn!("TAB: {}", tab.name); + let panes = tab.get_tiled_panes(); + let panes: Vec = panes + .map(|(_, p)| p.position_and_size().to_string()) + .collect(); + log::info!("***** PRINTING PANE GEOMS *****"); + for pane in panes { + log::info!("pane_geom: {}", pane); + } + log::info!("*******************************"); + } + // let layout: Vec<(&usize, BTreeMap>)> = screen + // .tabs + // .values() + // .map(|t| (t.position, t.get_tiled_panes())) + // .collect(); + // let tab_names: Vec<(String, Vec)> = + // layout.iter().map(|(t, p)| (t, p.clone())).collect(); screen.unblock_input()?; screen.render()?; }, diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index d3a26b40dc..62b4c40579 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -1769,7 +1769,7 @@ impl Tab { } } } - fn get_tiled_panes(&self) -> impl Iterator)> { + pub(crate) fn get_tiled_panes(&self) -> impl Iterator)> { self.tiled_panes.get_panes() } fn get_selectable_tiled_panes(&self) -> impl Iterator)> { diff --git a/zellij-utils/src/home.rs b/zellij-utils/src/home.rs index 8e7190ba27..c4bea55d53 100644 --- a/zellij-utils/src/home.rs +++ b/zellij-utils/src/home.rs @@ -1,4 +1,4 @@ -//! +//! //! # This module contain everything you'll need to access local system paths //! containing configuration and layouts @@ -100,6 +100,10 @@ pub fn home_config_dir() -> Option { } } +pub fn default_layout_dir() -> Option { + find_default_config_dir().map(|dir| dir.join("layouts")) +} + pub fn get_layout_dir(config_dir: Option) -> Option { config_dir.map(|dir| dir.join("layouts")) } @@ -107,3 +111,7 @@ pub fn get_layout_dir(config_dir: Option) -> Option { pub fn get_theme_dir(config_dir: Option) -> Option { config_dir.map(|dir| dir.join("themes")) } + +pub fn default_theme_dir() -> Option { + find_default_config_dir().map(|dir| dir.join("themes")) +} diff --git a/zellij-utils/src/pane_size.rs b/zellij-utils/src/pane_size.rs index 1df73a32db..f3880ddd9c 100644 --- a/zellij-utils/src/pane_size.rs +++ b/zellij-utils/src/pane_size.rs @@ -1,5 +1,8 @@ use serde::{Deserialize, Serialize}; -use std::hash::{Hash, Hasher}; +use std::{ + fmt::Display, + hash::{Hash, Hasher}, +}; use crate::position::Position; @@ -45,7 +48,7 @@ pub struct SizeInPixels { #[derive(Eq, Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Hash)] pub struct Dimension { pub constraint: Constraint, - inner: usize, + pub(crate) inner: usize, } impl Default for Dimension { @@ -131,6 +134,17 @@ pub enum Constraint { Percent(f64), } +impl Display for Constraint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let actual = match self { + Constraint::Fixed(v) => *v as f64, + Constraint::Percent(v) => *v, + }; + write!(f, "{}", actual)?; + Ok(()) + } +} + #[allow(clippy::derive_hash_xor_eq)] impl Hash for Constraint { fn hash(&self, state: &mut H) { @@ -157,6 +171,20 @@ impl PaneGeom { } } +impl Display for PaneGeom { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{{ ")?; + write!(f, "\"x\": {},", self.x)?; + write!(f, "\"y\": {},", self.y)?; + write!(f, "\"cols\": {},", self.cols.constraint)?; + write!(f, "\"rows\": {},", self.rows.constraint)?; + write!(f, "\"stacked\": {}", self.is_stacked)?; + write!(f, " }}")?; + + Ok(()) + } +} + impl Offset { pub fn frame(size: usize) -> Self { Self { diff --git a/zellij-utils/src/persistence.rs b/zellij-utils/src/persistence.rs new file mode 100644 index 0000000000..fc9fbc1d68 --- /dev/null +++ b/zellij-utils/src/persistence.rs @@ -0,0 +1,257 @@ +//! # Persistence module +//! !WIP! This module is holding the logic for all persistence sessions need +//! +//! # Examples +//! ```rust,no_run +//! fn main() { +//! // Set test data +//! let vec_string_geoms = vec![ +//! r#"[ {"x": 0, "y": 0, "cols": 100, "rows": 50}, {"x": 0, "y": 50, "rows": 50, "cols": 50}, {"x": 50, "y": 50, "rows": 50, "cols": 50} ]"#, +//! r#"[{"x": 0, "y": 0, "cols": 80, "rows": 30}, {"x": 0, "y": 30, "rows": 30, "cols": 30}, {"x": 30, "y": 30, "rows": 30, "cols": 50}]"#, +//! r#"[{"x": 0, "y": 0, "cols": 60, "rows": 40}, {"x": 60, "y": 0, "rows": 40, "cols": 20}, {"x": 0, "y": 40, "rows": 20, "cols": 60}, {"x": 60, "y": 40, "rows": 20, "cols": 20}]"#, +//! r#"[{"x": 0, "y": 0, "cols": 40, "rows": 20}, {"x": 40, "y": 0, "rows": 20, "cols": 40}, {"x": 0, "y": 20, "rows": 20, "cols": 25}, {"x": 25, "y": 20, "rows": 20, "cols": 30}, {"x": 55, "y": 20, "rows": 20, "cols": 25}, {"x": 0, "y": 40, "rows": 20, "cols": 40}, {"x": 40, "y": 40, "rows": 20, "cols": 40}]"#, +//! r#"[{"x": 0, "y": 0, "cols": 40, "rows": 30}, {"x": 0, "y": 30, "cols": 40, "rows": 30}, {"x": 40, "y": 0, "cols": 40, "rows":20}, {"x": 40, "y": 20, "cols": 20, "rows": 20}, {"x": 60, "y": 20, "cols": 20, "rows": 20}, {"x": 40, "y": 40, "cols": 40, "rows": 20}]"#, +//! r#"[{"x": 0, "y": 0, "cols": 30, "rows": 20}, {"x": 0, "y": 20, "cols": 30, "rows": 20}, {"x": 0, "y": 40, "cols": 30, "rows": 10}, {"x": 30, "y": 0, "cols": 30, "rows": 50}, {"x": 0, "y": 50, "cols": 60, "rows": 10}, {"x": 60, "y": 0, "cols": 20, "rows": 60}]"#, +//! ]; +//! let vec_hashmap_geoms: Vec>> = vec_string_geoms +//! .iter() +//! .map(|s| serde_json::from_str(s).unwrap()) +//! .collect(); +//! let vec_geoms: Vec> = vec_hashmap_geoms +//! .iter() +//! .map(|hms| { +//! hms.iter() +//! .map(|hm| get_panegeom_from_hashmap(&hm)) +//! .collect() +//! }) +//! .collect(); +//! +//! for (i, geoms) in vec_geoms.iter().enumerate() { +//! let kdl_string = kdl_string_from_geoms(&geoms); +//! println!("========== {i} =========="); +//! println!("{kdl_string}\n"); +//! } +//! } +//! ``` +//! +use crate::{ + input::layout::{SplitDirection, SplitSize, TiledPaneLayout}, + pane_size::{Dimension, PaneGeom}, +}; +use std::collections::HashMap; + + + +pub fn get_panegeom_from_hashmap(hm: &HashMap) -> PaneGeom { + PaneGeom { + x: hm["x"] as usize, + y: hm["y"] as usize, + rows: Dimension::fixed(hm["rows"] as usize), + cols: Dimension::fixed(hm["cols"] as usize), + is_stacked: false, + } +} + +pub fn kdl_string_from_geoms(geoms: &Vec) -> String { + let layout = get_layout_from_geoms(&geoms, None); + let tab = if &layout.children_split_direction != &SplitDirection::default() { + vec![layout] + } else { + layout.children + }; + kdl_string_from_tab(&tab) +} + +fn kdl_string_from_tab(tab: &Vec) -> String { + let mut kdl_string = String::from("tab {\n"); + let indent = " "; + let indent_level = 1; + for layout in tab { + kdl_string.push_str(&kdl_string_from_layout(&layout, indent, indent_level)); + } + kdl_string.push_str("}"); + kdl_string +} + +fn kdl_string_from_layout(layout: &TiledPaneLayout, indent: &str, indent_level: usize) -> String { + let mut kdl_string = String::from(&indent.repeat(indent_level)); + kdl_string.push_str("pane "); + match layout.split_size { + Some(SplitSize::Fixed(size)) => kdl_string.push_str(&format!("size={size} ")), + Some(SplitSize::Percent(size)) => kdl_string.push_str(&format!("size={size}% ")), + None => (), + }; + if layout.children_split_direction != SplitDirection::default() { + let direction = match layout.children_split_direction { + SplitDirection::Horizontal => "horizontal", + SplitDirection::Vertical => "vertical", + }; + kdl_string.push_str(&format!("split_direction=\"{direction}\" ")); + } + if layout.children.is_empty() { + kdl_string.push_str("\n"); + } else { + kdl_string.push_str("{\n"); + for pane in &layout.children { + kdl_string.push_str(&kdl_string_from_layout(&pane, indent, indent_level + 1)); + } + kdl_string.push_str(&indent.repeat(indent_level)); + kdl_string.push_str("}\n"); + } + kdl_string +} + +fn get_layout_from_geoms(geoms: &Vec, split_size: Option) -> TiledPaneLayout { + let (children_split_direction, splits) = match get_splits(&geoms) { + Some(x) => x, + None => { + return TiledPaneLayout { + split_size, + ..Default::default() + } + }, + }; + let mut children = Vec::new(); + let mut remaining_geoms = geoms.clone(); + for i in 1..splits.len() { + let (v_min, v_max) = (splits[i - 1], splits[i]); + let subgeoms: Vec; + (subgeoms, remaining_geoms) = match children_split_direction { + SplitDirection::Horizontal => remaining_geoms + .clone() + .into_iter() + .partition(|g| g.y + g.rows.as_usize() <= v_max), + SplitDirection::Vertical => remaining_geoms + .clone() + .into_iter() + .partition(|g| g.x + g.cols.as_usize() <= v_max), + }; + let subsplit_size = SplitSize::Fixed(v_max - v_min); + children.push(get_layout_from_geoms(&subgeoms, Some(subsplit_size))); + } + TiledPaneLayout { + children_split_direction, + split_size, + children, + ..Default::default() + } +} + +fn get_x_lims(geoms: &Vec) -> Option<(usize, usize)> { + let x_min = geoms.iter().map(|g| g.x).min(); + let x_max = geoms.iter().map(|g| g.x + g.rows.as_usize()).max(); + match (x_min, x_max) { + (Some(x_min), Some(x_max)) => Some((x_min, x_max)), + _ => None, + } +} + +fn get_y_lims(geoms: &Vec) -> Option<(usize, usize)> { + let y_min = geoms.iter().map(|g| g.y).min(); + let y_max = geoms.iter().map(|g| g.y + g.rows.as_usize()).max(); + match (y_min, y_max) { + (Some(y_min), Some(y_max)) => Some((y_min, y_max)), + _ => None, + } +} + +fn get_splits(geoms: &Vec) -> Option<(SplitDirection, Vec)> { + if geoms.len() == 1 { + return None; + } + let (x_lims, y_lims) = match (get_x_lims(&geoms), get_y_lims(&geoms)) { + (Some(x_lims), Some(y_lims)) => (x_lims, y_lims), + _ => return None, + }; + let mut direction = SplitDirection::default(); + let mut splits = match direction { + SplitDirection::Vertical => get_vertical_splits(&geoms, x_lims, y_lims), + SplitDirection::Horizontal => get_horizontal_splits(&geoms, x_lims, y_lims), + }; + if splits.len() <= 2 { + direction = !direction; + splits = match direction { + SplitDirection::Vertical => get_vertical_splits(&geoms, x_lims, y_lims), + SplitDirection::Horizontal => get_horizontal_splits(&geoms, x_lims, y_lims), + }; + } + if splits.len() <= 2 { + None + } else { + Some((direction, splits)) + } +} + +fn get_vertical_splits( + geoms: &Vec, + x_lims: (usize, usize), + y_lims: (usize, usize), +) -> Vec { + let ((_, x_max), (y_min, y_max)) = (x_lims, y_lims); + let height = y_max - y_min; + let mut splits = Vec::new(); + for x in geoms.iter().map(|g| g.x) { + if splits.contains(&x) { + continue; + } + if geoms + .iter() + .filter(|g| g.x == x) + .map(|g| g.rows.as_usize()) + .sum::() + == height + { + splits.push(x); + }; + } + splits.push(x_max); + splits +} + +fn get_horizontal_splits( + geoms: &Vec, + x_lims: (usize, usize), + y_lims: (usize, usize), +) -> Vec { + let ((x_min, x_max), (_, y_max)) = (x_lims, y_lims); + let width = x_max - x_min; + let mut splits = Vec::new(); + for y in geoms.iter().map(|g| g.y) { + if splits.contains(&y) { + continue; + } + if geoms + .iter() + .filter(|g| g.y == y) + .map(|g| g.cols.as_usize()) + .sum::() + == width + { + splits.push(y); + }; + } + splits.push(y_max); + splits +} + +#[cfg(test)] +mod tests { + use super::*; + + const DIM: Dimension = Dimension { + constraint: crate::pane_size::Constraint::Fixed(0), + inner: 0, + }; + const PANE_GEOM: PaneGeom = PaneGeom { + x: 0, + y: 0, + rows: DIM, + cols: DIM, + is_stacked: false, + }; + + #[test] + fn geoms() { + assert!(true); + } +} diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 88628b97b6..50e0a441ee 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -128,18 +128,23 @@ pub fn dump_specified_layout(layout: &str) -> std::io::Result<()> { "disable-status" => dump_asset(NO_STATUS_LAYOUT), "current" => { info!("DUMP CURRENT"); + // TODO: use from utils Ok(()) }, custom => { info!("Dump {custom} layout"); - let dir = find_default_config_dir(); - let path = Some(PathBuf::from(custom)); - let Ok((layout_path, ..)) = Layout::stringified_from_path_or_default(path.as_ref(), dir) else { - log::error!("No layout named {custom} found"); - return Ok(()) - }; - std::io::stdout().write_all(layout_path.as_bytes())?; - Ok(()) + let home = default_layout_dir(); + let path = home.map(|h| h.join(custom)); + let layout_exists = path.as_ref().map(|p| p.exists()).unwrap_or_default(); + match (path, layout_exists) { + (Some(layout), true) => { + std::io::stdout().write_all(layout.to_string_lossy().as_bytes()) + }, + _ => { + log::error!("No layout named {custom} found"); + return Ok(()); + }, + } }, } } From 71cc15b7bae17f2550469fb0772fd7c958eb28bb Mon Sep 17 00:00:00 2001 From: alekspickle Date: Thu, 29 Jun 2023 11:21:44 +0200 Subject: [PATCH 005/128] add tests; print resulting kdl --- Cargo.lock | 17 +++++++ Cargo.toml | 8 +-- zellij-server/src/screen.rs | 26 +++++----- zellij-utils/Cargo.toml | 2 +- zellij-utils/src/home.rs | 8 +-- zellij-utils/src/pane_size.rs | 10 ++-- zellij-utils/src/persistence.rs | 88 ++++++++++++++++++++++++++------- zellij-utils/src/setup.rs | 5 -- 8 files changed, 113 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5d3731ba4..d3e1d0be07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -888,6 +888,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" +[[package]] +name = "dissimilar" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210ec60ae7d710bed8683e333e9d2855a8a56a3e9892b38bad3bb0d4d29b0d5e" + [[package]] name = "dynasm" version = "1.2.3" @@ -1003,6 +1009,16 @@ version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" +[[package]] +name = "expect-test" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d9eafeadd538e68fb28016364c9732d78e420b9ff8853fa5e4058861e9f8d3" +dependencies = [ + "dissimilar", + "once_cell", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -4505,6 +4521,7 @@ dependencies = [ "colorsys", "crossbeam", "directories-next", + "expect-test", "include_dir", "insta", "interprocess", diff --git a/Cargo.toml b/Cargo.toml index 75e1864b5e..acdf211876 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,10 +76,10 @@ pkg-fmt = "tgz" [features] # See remarks in zellij_utils/Cargo.toml -default = [ "zellij-utils/plugins_from_target" ] -disable_automatic_asset_installation = [ "zellij-utils/disable_automatic_asset_installation" ] -unstable = [ "zellij-client/unstable", "zellij-utils/unstable" ] -singlepass = [ "zellij-server/singlepass" ] +default = ["zellij-utils/plugins_from_target"] +disable_automatic_asset_installation = ["zellij-utils/disable_automatic_asset_installation"] +unstable = ["zellij-client/unstable", "zellij-utils/unstable"] +singlepass = ["zellij-server/singlepass"] # uncomment this when developing plugins in the Zellij UI to make plugin compilation faster # [profile.dev.package."*"] diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 5305704765..cfdcd21fd4 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -1931,23 +1931,19 @@ pub(crate) fn screen_thread_main( ScreenInstruction::DumpLayout(client_id, layout) => { for tab in screen.tabs.values() { log::warn!("TAB: {}", tab.name); - let panes = tab.get_tiled_panes(); - let panes: Vec = panes - .map(|(_, p)| p.position_and_size().to_string()) + let panes_str: String = tab + .get_tiled_panes() + .map(|(_, p)| p.position_and_size()) + .map(|p| p.to_string()) .collect(); - log::info!("***** PRINTING PANE GEOMS *****"); - for pane in panes { - log::info!("pane_geom: {}", pane); - } - log::info!("*******************************"); + log::info!("PANES: {panes_str}"); + let panes: Vec = tab + .get_tiled_panes() + .map(|(_, p)| p.position_and_size()) + .collect(); + let kdl_config = persistence::geoms_to_kdl(&tab.name, &panes); + log::info!("{kdl_config}"); } - // let layout: Vec<(&usize, BTreeMap>)> = screen - // .tabs - // .values() - // .map(|t| (t.position, t.get_tiled_panes())) - // .collect(); - // let tab_names: Vec<(String, Vec)> = - // layout.iter().map(|(t, p)| (t, p.clone())).collect(); screen.unblock_input()?; screen.render()?; }, diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index 9b260d1cbb..2877334dbe 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -54,7 +54,7 @@ notify-debouncer-full = "0.1.0" [dev-dependencies] insta = { version = "1.6.0", features = ["backtrace"] } - +expect-test = "1.4.1" [features] # If this feature is NOT set (default): diff --git a/zellij-utils/src/home.rs b/zellij-utils/src/home.rs index c4bea55d53..2976f8b209 100644 --- a/zellij-utils/src/home.rs +++ b/zellij-utils/src/home.rs @@ -100,14 +100,14 @@ pub fn home_config_dir() -> Option { } } -pub fn default_layout_dir() -> Option { - find_default_config_dir().map(|dir| dir.join("layouts")) -} - pub fn get_layout_dir(config_dir: Option) -> Option { config_dir.map(|dir| dir.join("layouts")) } +pub fn default_layout_dir() -> Option { + find_default_config_dir().map(|dir| dir.join("layouts")) +} + pub fn get_theme_dir(config_dir: Option) -> Option { config_dir.map(|dir| dir.join("themes")) } diff --git a/zellij-utils/src/pane_size.rs b/zellij-utils/src/pane_size.rs index f3880ddd9c..8e68c3863b 100644 --- a/zellij-utils/src/pane_size.rs +++ b/zellij-utils/src/pane_size.rs @@ -174,11 +174,11 @@ impl PaneGeom { impl Display for PaneGeom { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{{ ")?; - write!(f, "\"x\": {},", self.x)?; - write!(f, "\"y\": {},", self.y)?; - write!(f, "\"cols\": {},", self.cols.constraint)?; - write!(f, "\"rows\": {},", self.rows.constraint)?; - write!(f, "\"stacked\": {}", self.is_stacked)?; + write!(f, r#""x": {},"#, self.x)?; + write!(f, r#""y": {},"#, self.y)?; + write!(f, r#""cols": {},"#, self.cols.constraint)?; + write!(f, r#""rows": {},"#, self.rows.constraint)?; + write!(f, r#""stacked": {}"#, self.is_stacked)?; write!(f, " }}")?; Ok(()) diff --git a/zellij-utils/src/persistence.rs b/zellij-utils/src/persistence.rs index fc9fbc1d68..da47a0eb67 100644 --- a/zellij-utils/src/persistence.rs +++ b/zellij-utils/src/persistence.rs @@ -21,13 +21,13 @@ //! .iter() //! .map(|hms| { //! hms.iter() -//! .map(|hm| get_panegeom_from_hashmap(&hm)) +//! .map(|hm| panegeom_from_hashmap(&hm)) //! .collect() //! }) //! .collect(); //! //! for (i, geoms) in vec_geoms.iter().enumerate() { -//! let kdl_string = kdl_string_from_geoms(&geoms); +//! let kdl_string = geoms_to_kdl(&geoms); //! println!("========== {i} =========="); //! println!("{kdl_string}\n"); //! } @@ -40,9 +40,20 @@ use crate::{ }; use std::collections::HashMap; +/// +/// Expects this input +/// +/// r#"[ {"x": 0, "y": 0, "cols": 100, "rows": 50}, {"x": 0, "y": 50, "rows": 50, "cols": 50}, {"x": 50, "y": 50, "rows": 50, "cols": 50} ]"# +/// +pub fn parse_geoms_from_json(geoms: &str) -> Vec { + let vec_hashmap_geoms: Vec> = serde_json::from_str(geoms).unwrap(); + vec_hashmap_geoms + .iter() + .map(panegeom_from_hashmap) + .collect() +} - -pub fn get_panegeom_from_hashmap(hm: &HashMap) -> PaneGeom { +pub fn panegeom_from_hashmap(hm: &HashMap) -> PaneGeom { PaneGeom { x: hm["x"] as usize, y: hm["y"] as usize, @@ -52,18 +63,18 @@ pub fn get_panegeom_from_hashmap(hm: &HashMap) -> PaneGeom { } } -pub fn kdl_string_from_geoms(geoms: &Vec) -> String { - let layout = get_layout_from_geoms(&geoms, None); +pub fn geoms_to_kdl(tab_name: &str, geoms: &[PaneGeom]) -> String { + let layout = get_layout_from_geoms(geoms, None); let tab = if &layout.children_split_direction != &SplitDirection::default() { vec![layout] } else { layout.children }; - kdl_string_from_tab(&tab) + geoms_to_kdl_tab(tab_name, &tab) } -fn kdl_string_from_tab(tab: &Vec) -> String { - let mut kdl_string = String::from("tab {\n"); +fn geoms_to_kdl_tab(name: &str, tab: &[TiledPaneLayout]) -> String { + let mut kdl_string = format!("tab name=\"{name}\"{{\n"); let indent = " "; let indent_level = 1; for layout in tab { @@ -101,7 +112,7 @@ fn kdl_string_from_layout(layout: &TiledPaneLayout, indent: &str, indent_level: kdl_string } -fn get_layout_from_geoms(geoms: &Vec, split_size: Option) -> TiledPaneLayout { +fn get_layout_from_geoms(geoms: &[PaneGeom], split_size: Option) -> TiledPaneLayout { let (children_split_direction, splits) = match get_splits(&geoms) { Some(x) => x, None => { @@ -112,7 +123,7 @@ fn get_layout_from_geoms(geoms: &Vec, split_size: Option) - }, }; let mut children = Vec::new(); - let mut remaining_geoms = geoms.clone(); + let mut remaining_geoms = geoms.to_owned(); for i in 1..splits.len() { let (v_min, v_max) = (splits[i - 1], splits[i]); let subgeoms: Vec; @@ -137,7 +148,7 @@ fn get_layout_from_geoms(geoms: &Vec, split_size: Option) - } } -fn get_x_lims(geoms: &Vec) -> Option<(usize, usize)> { +fn get_x_lims(geoms: &[PaneGeom]) -> Option<(usize, usize)> { let x_min = geoms.iter().map(|g| g.x).min(); let x_max = geoms.iter().map(|g| g.x + g.rows.as_usize()).max(); match (x_min, x_max) { @@ -146,7 +157,7 @@ fn get_x_lims(geoms: &Vec) -> Option<(usize, usize)> { } } -fn get_y_lims(geoms: &Vec) -> Option<(usize, usize)> { +fn get_y_lims(geoms: &[PaneGeom]) -> Option<(usize, usize)> { let y_min = geoms.iter().map(|g| g.y).min(); let y_max = geoms.iter().map(|g| g.y + g.rows.as_usize()).max(); match (y_min, y_max) { @@ -155,7 +166,7 @@ fn get_y_lims(geoms: &Vec) -> Option<(usize, usize)> { } } -fn get_splits(geoms: &Vec) -> Option<(SplitDirection, Vec)> { +fn get_splits(geoms: &[PaneGeom]) -> Option<(SplitDirection, Vec)> { if geoms.len() == 1 { return None; } @@ -183,7 +194,7 @@ fn get_splits(geoms: &Vec) -> Option<(SplitDirection, Vec)> { } fn get_vertical_splits( - geoms: &Vec, + geoms: &[PaneGeom], x_lims: (usize, usize), y_lims: (usize, usize), ) -> Vec { @@ -209,7 +220,7 @@ fn get_vertical_splits( } fn get_horizontal_splits( - geoms: &Vec, + geoms: &[PaneGeom], x_lims: (usize, usize), y_lims: (usize, usize), ) -> Vec { @@ -237,7 +248,16 @@ fn get_horizontal_splits( #[cfg(test)] mod tests { use super::*; + use expect_test::expect; + const LAYOUT: &[&str] = &[ + r#"[ {"x": 0, "y": 0, "cols": 100, "rows": 50}, {"x": 0, "y": 50, "rows": 50, "cols": 50}, {"x": 50, "y": 50, "rows": 50, "cols": 50} ]"#, + r#"[{"x": 0, "y": 0, "cols": 80, "rows": 30}, {"x": 0, "y": 30, "rows": 30, "cols": 30}, {"x": 30, "y": 30, "rows": 30, "cols": 50}]"#, + r#"[{"x": 0, "y": 0, "cols": 60, "rows": 40}, {"x": 60, "y": 0, "rows": 40, "cols": 20}, {"x": 0, "y": 40, "rows": 20, "cols": 60}, {"x": 60, "y": 40, "rows": 20, "cols": 20}]"#, + r#"[{"x": 0, "y": 0, "cols": 40, "rows": 20}, {"x": 40, "y": 0, "rows": 20, "cols": 40}, {"x": 0, "y": 20, "rows": 20, "cols": 25}, {"x": 25, "y": 20, "rows": 20, "cols": 30}, {"x": 55, "y": 20, "rows": 20, "cols": 25}, {"x": 0, "y": 40, "rows": 20, "cols": 40}, {"x": 40, "y": 40, "rows": 20, "cols": 40}]"#, + r#"[{"x": 0, "y": 0, "cols": 40, "rows": 30}, {"x": 0, "y": 30, "cols": 40, "rows": 30}, {"x": 40, "y": 0, "cols": 40, "rows":20}, {"x": 40, "y": 20, "cols": 20, "rows": 20}, {"x": 60, "y": 20, "cols": 20, "rows": 20}, {"x": 40, "y": 40, "cols": 40, "rows": 20}]"#, + r#"[{"x": 0, "y": 0, "cols": 30, "rows": 20}, {"x": 0, "y": 20, "cols": 30, "rows": 20}, {"x": 0, "y": 40, "cols": 30, "rows": 10}, {"x": 30, "y": 0, "cols": 30, "rows": 50}, {"x": 0, "y": 50, "cols": 60, "rows": 10}, {"x": 60, "y": 0, "cols": 20, "rows": 60}]"#, + ]; const DIM: Dimension = Dimension { constraint: crate::pane_size::Constraint::Fixed(0), inner: 0, @@ -252,6 +272,40 @@ mod tests { #[test] fn geoms() { - assert!(true); + let geoms = parse_geoms_from_json(LAYOUT[0]); + let kdl = geoms_to_kdl("test", &geoms); + expect![[r#" + "tab name=\"test\"{\n pane size=50 \n pane size=50 split_direction=\"vertical\" {\n pane size=50 \n pane size=50 \n }\n}" + "#]].assert_debug_eq(&kdl); + + let geoms = parse_geoms_from_json(LAYOUT[1]); + let kdl = geoms_to_kdl("test", &geoms); + expect![[r#" + "tab name=\"test\"{\n}" + "#]].assert_debug_eq(&kdl); + + let geoms = parse_geoms_from_json(LAYOUT[2]); + let kdl = geoms_to_kdl("test", &geoms); + expect![[r#" + "tab name=\"test\"{\n pane split_direction=\"vertical\" {\n pane size=60 \n pane size=40 \n }\n}" + "#]].assert_debug_eq(&kdl); + + let geoms = parse_geoms_from_json(LAYOUT[3]); + let kdl = geoms_to_kdl("test", &geoms); + expect![[r#" + "tab name=\"test\"{\n}" + "#]].assert_debug_eq(&kdl); + + let geoms = parse_geoms_from_json(LAYOUT[4]); + let kdl = geoms_to_kdl("test", &geoms); + expect![[r#" + "tab name=\"test\"{\n pane split_direction=\"vertical\" {\n pane size=40 \n pane size=40 {\n pane size=20 \n pane size=20 split_direction=\"vertical\" {\n pane size=20 \n pane size=20 \n }\n pane size=20 \n }\n }\n}" + "#]].assert_debug_eq(&kdl); + + let geoms = parse_geoms_from_json(LAYOUT[5]); + let kdl = geoms_to_kdl("test", &geoms); + expect![[r#" + "tab name=\"test\"{\n pane split_direction=\"vertical\" {\n pane size=60 \n pane size=60 \n }\n}" + "#]].assert_debug_eq(&kdl); } } diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 50e0a441ee..86287308f5 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -126,11 +126,6 @@ pub fn dump_specified_layout(layout: &str) -> std::io::Result<()> { "default" => dump_asset(DEFAULT_LAYOUT), "compact" => dump_asset(COMPACT_BAR_LAYOUT), "disable-status" => dump_asset(NO_STATUS_LAYOUT), - "current" => { - info!("DUMP CURRENT"); - // TODO: use from utils - Ok(()) - }, custom => { info!("Dump {custom} layout"); let home = default_layout_dir(); From a58c9450f6f9aeb8ee99456a383aa3cd3b0bb277 Mon Sep 17 00:00:00 2001 From: alekspickle Date: Sun, 2 Jul 2023 22:22:54 +0200 Subject: [PATCH 006/128] fix dumping custom layouts from setup; start fixing algorithm for simplest layout possible --- zellij-server/src/screen.rs | 30 ++--- zellij-utils/src/persistence.rs | 189 +++++++++++++++++++++----------- zellij-utils/src/setup.rs | 24 +++- 3 files changed, 160 insertions(+), 83 deletions(-) diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index cfdcd21fd4..97802cb451 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -1929,21 +1929,21 @@ pub(crate) fn screen_thread_main( screen.unblock_input()?; }, ScreenInstruction::DumpLayout(client_id, layout) => { - for tab in screen.tabs.values() { - log::warn!("TAB: {}", tab.name); - let panes_str: String = tab - .get_tiled_panes() - .map(|(_, p)| p.position_and_size()) - .map(|p| p.to_string()) - .collect(); - log::info!("PANES: {panes_str}"); - let panes: Vec = tab - .get_tiled_panes() - .map(|(_, p)| p.position_and_size()) - .collect(); - let kdl_config = persistence::geoms_to_kdl(&tab.name, &panes); - log::info!("{kdl_config}"); - } + let tabs: Vec<(String, Vec)> = screen + .tabs + .values() + .into_iter() + .map(|tab| { + let panes: Vec = tab + .get_tiled_panes() + .map(|(_, p)| p.position_and_size()) + .collect(); + (tab.name.clone(), panes) + }) + .collect(); + let kdl_config = persistence::tabs_to_kdl(&tabs); + + log::info!("{kdl_config}"); screen.unblock_input()?; screen.render()?; }, diff --git a/zellij-utils/src/persistence.rs b/zellij-utils/src/persistence.rs index da47a0eb67..693bb42713 100644 --- a/zellij-utils/src/persistence.rs +++ b/zellij-utils/src/persistence.rs @@ -27,7 +27,7 @@ //! .collect(); //! //! for (i, geoms) in vec_geoms.iter().enumerate() { -//! let kdl_string = geoms_to_kdl(&geoms); +//! let kdl_string = geoms_to_kdl_tab(&geoms); //! println!("========== {i} =========="); //! println!("{kdl_string}\n"); //! } @@ -40,6 +40,23 @@ use crate::{ }; use std::collections::HashMap; +const INDENT: &str = " "; + +pub fn tabs_to_kdl(tabs: &[(String, Vec)]) -> String { + let tab_n = tabs.len(); + let mut kdl_layout = format!("layout {{\n"); + + for (name, panes) in tabs { + // log::info!("PANES in tab:{panes:?}"); + let mut kdl_tab = geoms_to_kdl_tab(&name, &panes, tab_n); + kdl_tab.push_str("\n") + } + + kdl_layout.push_str("}"); + + kdl_layout +} + /// /// Expects this input /// @@ -53,39 +70,49 @@ pub fn parse_geoms_from_json(geoms: &str) -> Vec { .collect() } -pub fn panegeom_from_hashmap(hm: &HashMap) -> PaneGeom { +pub fn panegeom_from_hashmap(map: &HashMap) -> PaneGeom { PaneGeom { - x: hm["x"] as usize, - y: hm["y"] as usize, - rows: Dimension::fixed(hm["rows"] as usize), - cols: Dimension::fixed(hm["cols"] as usize), + x: map["x"] as usize, + y: map["y"] as usize, + rows: Dimension::fixed(map["rows"] as usize), + cols: Dimension::fixed(map["cols"] as usize), is_stacked: false, } } -pub fn geoms_to_kdl(tab_name: &str, geoms: &[PaneGeom]) -> String { - let layout = get_layout_from_geoms(geoms, None); +/// Tab declaration +fn geoms_to_kdl_tab(name: &str, geoms: &[PaneGeom], tab_n: usize) -> String { + let layout = layout_from_geoms(geoms, None); + // log::info!("TiledLayout: {layout:?}"); let tab = if &layout.children_split_direction != &SplitDirection::default() { vec![layout] } else { layout.children }; - geoms_to_kdl_tab(tab_name, &tab) -} -fn geoms_to_kdl_tab(name: &str, tab: &[TiledPaneLayout]) -> String { - let mut kdl_string = format!("tab name=\"{name}\"{{\n"); - let indent = " "; + // skip tab decl if the tab is the only one + let mut kdl_string = match tab_n { + 1 => format!(""), + _ => format!("tab name=\"{name}\" {{\n"), + }; + let indent_level = 1; for layout in tab { - kdl_string.push_str(&kdl_string_from_layout(&layout, indent, indent_level)); + kdl_string.push_str(&kdl_string_from_layout(&layout, indent_level)); } - kdl_string.push_str("}"); + + // skip tab closing } if the tab is the only one + match tab_n { + 1 => {}, + _ => kdl_string.push_str("}"), + }; + kdl_string } -fn kdl_string_from_layout(layout: &TiledPaneLayout, indent: &str, indent_level: usize) -> String { - let mut kdl_string = String::from(&indent.repeat(indent_level)); +/// Pane declaration and recursion +fn kdl_string_from_layout(layout: &TiledPaneLayout, indent_level: usize) -> String { + let mut kdl_string = String::from(&INDENT.repeat(indent_level)); kdl_string.push_str("pane "); match layout.split_size { Some(SplitSize::Fixed(size)) => kdl_string.push_str(&format!("size={size} ")), @@ -104,16 +131,17 @@ fn kdl_string_from_layout(layout: &TiledPaneLayout, indent: &str, indent_level: } else { kdl_string.push_str("{\n"); for pane in &layout.children { - kdl_string.push_str(&kdl_string_from_layout(&pane, indent, indent_level + 1)); + kdl_string.push_str(&kdl_string_from_layout(&pane, indent_level + 1)); } - kdl_string.push_str(&indent.repeat(indent_level)); + kdl_string.push_str(&INDENT.repeat(indent_level)); kdl_string.push_str("}\n"); } kdl_string } -fn get_layout_from_geoms(geoms: &[PaneGeom], split_size: Option) -> TiledPaneLayout { - let (children_split_direction, splits) = match get_splits(&geoms) { +/// Tab-level parsing +fn layout_from_geoms(geoms: &[PaneGeom], split_size: Option) -> TiledPaneLayout { + let (children_split_direction, splits) = match splits(&geoms) { Some(x) => x, None => { return TiledPaneLayout { @@ -122,24 +150,34 @@ fn get_layout_from_geoms(geoms: &[PaneGeom], split_size: Option) -> T } }, }; - let mut children = Vec::new(); + log::info!("SPLITS: {splits:?}"); + let mut remaining_geoms = geoms.to_owned(); - for i in 1..splits.len() { - let (v_min, v_max) = (splits[i - 1], splits[i]); - let subgeoms: Vec; - (subgeoms, remaining_geoms) = match children_split_direction { - SplitDirection::Horizontal => remaining_geoms - .clone() - .into_iter() - .partition(|g| g.y + g.rows.as_usize() <= v_max), - SplitDirection::Vertical => remaining_geoms - .clone() + let children = match splits { + splits if splits.len() == 1 => { + eprintln!("HERE"); + vec![layout_from_subgeoms( + &mut remaining_geoms, + children_split_direction, + (0, splits[0]), + )] + }, + _ => { + eprintln!("OR HERE"); + + (1..splits.len()) .into_iter() - .partition(|g| g.x + g.cols.as_usize() <= v_max), - }; - let subsplit_size = SplitSize::Fixed(v_max - v_min); - children.push(get_layout_from_geoms(&subgeoms, Some(subsplit_size))); - } + .map(|i| { + let (v_min, v_max) = (splits[i - 1], splits[i]); + layout_from_subgeoms( + &mut remaining_geoms, + children_split_direction, + (v_min, v_max), + ) + }) + .collect() + }, + }; TiledPaneLayout { children_split_direction, split_size, @@ -148,7 +186,27 @@ fn get_layout_from_geoms(geoms: &[PaneGeom], split_size: Option) -> T } } -fn get_x_lims(geoms: &[PaneGeom]) -> Option<(usize, usize)> { +fn layout_from_subgeoms( + remaining_geoms: &mut Vec, + split_direction: SplitDirection, + (v_min, v_max): (usize, usize), +) -> TiledPaneLayout { + let subgeoms: Vec; + (subgeoms, *remaining_geoms) = match split_direction { + SplitDirection::Horizontal => remaining_geoms + .clone() + .into_iter() + .partition(|g| g.y + g.rows.as_usize() <= v_max), + SplitDirection::Vertical => remaining_geoms + .clone() + .into_iter() + .partition(|g| g.x + g.cols.as_usize() <= v_max), + }; + let subsplit_size = SplitSize::Fixed(v_max - v_min); + layout_from_geoms(&subgeoms, Some(subsplit_size)) +} + +fn x_lims(geoms: &[PaneGeom]) -> Option<(usize, usize)> { let x_min = geoms.iter().map(|g| g.x).min(); let x_max = geoms.iter().map(|g| g.x + g.rows.as_usize()).max(); match (x_min, x_max) { @@ -157,7 +215,7 @@ fn get_x_lims(geoms: &[PaneGeom]) -> Option<(usize, usize)> { } } -fn get_y_lims(geoms: &[PaneGeom]) -> Option<(usize, usize)> { +fn y_lims(geoms: &[PaneGeom]) -> Option<(usize, usize)> { let y_min = geoms.iter().map(|g| g.y).min(); let y_max = geoms.iter().map(|g| g.y + g.rows.as_usize()).max(); match (y_min, y_max) { @@ -166,26 +224,29 @@ fn get_y_lims(geoms: &[PaneGeom]) -> Option<(usize, usize)> { } } -fn get_splits(geoms: &[PaneGeom]) -> Option<(SplitDirection, Vec)> { +fn splits(geoms: &[PaneGeom]) -> Option<(SplitDirection, Vec)> { + log::info!("len: {}", geoms.len()); if geoms.len() == 1 { return None; } - let (x_lims, y_lims) = match (get_x_lims(&geoms), get_y_lims(&geoms)) { + let (x_lims, y_lims) = match (x_lims(&geoms), y_lims(&geoms)) { (Some(x_lims), Some(y_lims)) => (x_lims, y_lims), _ => return None, }; let mut direction = SplitDirection::default(); let mut splits = match direction { - SplitDirection::Vertical => get_vertical_splits(&geoms, x_lims, y_lims), - SplitDirection::Horizontal => get_horizontal_splits(&geoms, x_lims, y_lims), + SplitDirection::Vertical => vertical_splits(&geoms, x_lims, y_lims), + SplitDirection::Horizontal => horizontal_splits(&geoms, x_lims, y_lims), }; + log::info!("initial splits: {splits:?}, direction: {direction:?}"); if splits.len() <= 2 { direction = !direction; splits = match direction { - SplitDirection::Vertical => get_vertical_splits(&geoms, x_lims, y_lims), - SplitDirection::Horizontal => get_horizontal_splits(&geoms, x_lims, y_lims), + SplitDirection::Vertical => vertical_splits(&geoms, x_lims, y_lims), + SplitDirection::Horizontal => horizontal_splits(&geoms, x_lims, y_lims), }; } + log::info!("second step splits: {splits:?}"); if splits.len() <= 2 { None } else { @@ -193,11 +254,12 @@ fn get_splits(geoms: &[PaneGeom]) -> Option<(SplitDirection, Vec)> { } } -fn get_vertical_splits( +fn vertical_splits( geoms: &[PaneGeom], x_lims: (usize, usize), y_lims: (usize, usize), ) -> Vec { + log::info!("VERTICAL:{}", geoms.len()); let ((_, x_max), (y_min, y_max)) = (x_lims, y_lims); let height = y_max - y_min; let mut splits = Vec::new(); @@ -219,11 +281,12 @@ fn get_vertical_splits( splits } -fn get_horizontal_splits( +fn horizontal_splits( geoms: &[PaneGeom], x_lims: (usize, usize), y_lims: (usize, usize), ) -> Vec { + log::info!("HORIZONTAL:{}", geoms.len()); let ((x_min, x_max), (_, y_max)) = (x_lims, y_lims); let width = x_max - x_min; let mut splits = Vec::new(); @@ -259,7 +322,7 @@ mod tests { r#"[{"x": 0, "y": 0, "cols": 30, "rows": 20}, {"x": 0, "y": 20, "cols": 30, "rows": 20}, {"x": 0, "y": 40, "cols": 30, "rows": 10}, {"x": 30, "y": 0, "cols": 30, "rows": 50}, {"x": 0, "y": 50, "cols": 60, "rows": 10}, {"x": 60, "y": 0, "cols": 20, "rows": 60}]"#, ]; const DIM: Dimension = Dimension { - constraint: crate::pane_size::Constraint::Fixed(0), + constraint: crate::pane_size::Constraint::Fixed(5), inner: 0, }; const PANE_GEOM: PaneGeom = PaneGeom { @@ -273,39 +336,41 @@ mod tests { #[test] fn geoms() { let geoms = parse_geoms_from_json(LAYOUT[0]); - let kdl = geoms_to_kdl("test", &geoms); + let kdl = geoms_to_kdl_tab("test", &geoms, 2); expect![[r#" - "tab name=\"test\"{\n pane size=50 \n pane size=50 split_direction=\"vertical\" {\n pane size=50 \n pane size=50 \n }\n}" + "tab name=\"test\" {\n pane size=50 \n pane size=50 split_direction=\"vertical\" {\n pane size=50 \n pane size=50 \n }\n}" "#]].assert_debug_eq(&kdl); let geoms = parse_geoms_from_json(LAYOUT[1]); - let kdl = geoms_to_kdl("test", &geoms); + let kdl = geoms_to_kdl_tab("test", &geoms, 1); expect![[r#" - "tab name=\"test\"{\n}" - "#]].assert_debug_eq(&kdl); + "" + "#]] + .assert_debug_eq(&kdl); let geoms = parse_geoms_from_json(LAYOUT[2]); - let kdl = geoms_to_kdl("test", &geoms); + let kdl = geoms_to_kdl_tab("test", &geoms, 1); expect![[r#" - "tab name=\"test\"{\n pane split_direction=\"vertical\" {\n pane size=60 \n pane size=40 \n }\n}" + " pane split_direction=\"vertical\" {\n pane size=60 \n pane size=40 \n }\n" "#]].assert_debug_eq(&kdl); let geoms = parse_geoms_from_json(LAYOUT[3]); - let kdl = geoms_to_kdl("test", &geoms); + let kdl = geoms_to_kdl_tab("test", &geoms, 1); expect![[r#" - "tab name=\"test\"{\n}" - "#]].assert_debug_eq(&kdl); + "" + "#]] + .assert_debug_eq(&kdl); let geoms = parse_geoms_from_json(LAYOUT[4]); - let kdl = geoms_to_kdl("test", &geoms); + let kdl = geoms_to_kdl_tab("test", &geoms, 1); expect![[r#" - "tab name=\"test\"{\n pane split_direction=\"vertical\" {\n pane size=40 \n pane size=40 {\n pane size=20 \n pane size=20 split_direction=\"vertical\" {\n pane size=20 \n pane size=20 \n }\n pane size=20 \n }\n }\n}" + " pane split_direction=\"vertical\" {\n pane size=40 \n pane size=40 {\n pane size=20 \n pane size=20 split_direction=\"vertical\" {\n pane size=20 \n pane size=20 \n }\n pane size=20 \n }\n }\n" "#]].assert_debug_eq(&kdl); let geoms = parse_geoms_from_json(LAYOUT[5]); - let kdl = geoms_to_kdl("test", &geoms); + let kdl = geoms_to_kdl_tab("test", &geoms, 1); expect![[r#" - "tab name=\"test\"{\n pane split_direction=\"vertical\" {\n pane size=60 \n pane size=60 \n }\n}" + " pane split_direction=\"vertical\" {\n pane size=60 \n pane size=60 \n }\n" "#]].assert_debug_eq(&kdl); } } diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 86287308f5..5566f776e8 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -20,9 +20,7 @@ use clap_complete::Shell; use directories_next::BaseDirs; use log::info; use serde::{Deserialize, Serialize}; -use std::{ - convert::TryFrom, fmt::Write as FmtWrite, io::Write, path::Path, path::PathBuf, process, -}; +use std::{convert::TryFrom, fmt::Write as FmtWrite, fs, io::Write, path::PathBuf, process}; const CONFIG_NAME: &str = "config.kdl"; static ARROW_SEPARATOR: &str = ""; @@ -116,6 +114,17 @@ pub const ZSH_AUTO_START_SCRIPT: &[u8] = include_bytes!(concat!( "assets/shell/auto-start.zsh" )); +pub fn add_layout_ext(s: &str) -> String { + match s { + c if s.ends_with(".kdl") => c.to_owned(), + _ => { + let mut s = s.to_owned(); + s.push_str(".kdl"); + s + }, + } +} + pub fn dump_default_config() -> std::io::Result<()> { dump_asset(DEFAULT_CONFIG) } @@ -128,12 +137,15 @@ pub fn dump_specified_layout(layout: &str) -> std::io::Result<()> { "disable-status" => dump_asset(NO_STATUS_LAYOUT), custom => { info!("Dump {custom} layout"); + let custom = add_layout_ext(custom); let home = default_layout_dir(); - let path = home.map(|h| h.join(custom)); + let path = home.map(|h| h.join(&custom)); let layout_exists = path.as_ref().map(|p| p.exists()).unwrap_or_default(); + match (path, layout_exists) { - (Some(layout), true) => { - std::io::stdout().write_all(layout.to_string_lossy().as_bytes()) + (Some(path), true) => { + let content = fs::read_to_string(path)?; + std::io::stdout().write_all(content.as_bytes()) }, _ => { log::error!("No layout named {custom} found"); From 502fdc07ca85ab12d33c6ca595f85ab83aba8ce7 Mon Sep 17 00:00:00 2001 From: Example Name Date: Thu, 24 Aug 2023 16:52:37 +0700 Subject: [PATCH 007/128] fix: fixed persistence code and tests to support flexible layouts --- zellij-utils/src/persistence.rs | 593 ++++++++++++++++++++------------ 1 file changed, 370 insertions(+), 223 deletions(-) diff --git a/zellij-utils/src/persistence.rs b/zellij-utils/src/persistence.rs index 693bb42713..bf769c5e66 100644 --- a/zellij-utils/src/persistence.rs +++ b/zellij-utils/src/persistence.rs @@ -4,119 +4,123 @@ //! # Examples //! ```rust,no_run //! fn main() { -//! // Set test data -//! let vec_string_geoms = vec![ -//! r#"[ {"x": 0, "y": 0, "cols": 100, "rows": 50}, {"x": 0, "y": 50, "rows": 50, "cols": 50}, {"x": 50, "y": 50, "rows": 50, "cols": 50} ]"#, -//! r#"[{"x": 0, "y": 0, "cols": 80, "rows": 30}, {"x": 0, "y": 30, "rows": 30, "cols": 30}, {"x": 30, "y": 30, "rows": 30, "cols": 50}]"#, -//! r#"[{"x": 0, "y": 0, "cols": 60, "rows": 40}, {"x": 60, "y": 0, "rows": 40, "cols": 20}, {"x": 0, "y": 40, "rows": 20, "cols": 60}, {"x": 60, "y": 40, "rows": 20, "cols": 20}]"#, -//! r#"[{"x": 0, "y": 0, "cols": 40, "rows": 20}, {"x": 40, "y": 0, "rows": 20, "cols": 40}, {"x": 0, "y": 20, "rows": 20, "cols": 25}, {"x": 25, "y": 20, "rows": 20, "cols": 30}, {"x": 55, "y": 20, "rows": 20, "cols": 25}, {"x": 0, "y": 40, "rows": 20, "cols": 40}, {"x": 40, "y": 40, "rows": 20, "cols": 40}]"#, -//! r#"[{"x": 0, "y": 0, "cols": 40, "rows": 30}, {"x": 0, "y": 30, "cols": 40, "rows": 30}, {"x": 40, "y": 0, "cols": 40, "rows":20}, {"x": 40, "y": 20, "cols": 20, "rows": 20}, {"x": 60, "y": 20, "cols": 20, "rows": 20}, {"x": 40, "y": 40, "cols": 40, "rows": 20}]"#, -//! r#"[{"x": 0, "y": 0, "cols": 30, "rows": 20}, {"x": 0, "y": 20, "cols": 30, "rows": 20}, {"x": 0, "y": 40, "cols": 30, "rows": 10}, {"x": 30, "y": 0, "cols": 30, "rows": 50}, {"x": 0, "y": 50, "cols": 60, "rows": 10}, {"x": 60, "y": 0, "cols": 20, "rows": 60}]"#, -//! ]; -//! let vec_hashmap_geoms: Vec>> = vec_string_geoms -//! .iter() -//! .map(|s| serde_json::from_str(s).unwrap()) -//! .collect(); -//! let vec_geoms: Vec> = vec_hashmap_geoms -//! .iter() -//! .map(|hms| { -//! hms.iter() -//! .map(|hm| panegeom_from_hashmap(&hm)) -//! .collect() -//! }) -//! .collect(); -//! -//! for (i, geoms) in vec_geoms.iter().enumerate() { -//! let kdl_string = geoms_to_kdl_tab(&geoms); -//! println!("========== {i} =========="); -//! println!("{kdl_string}\n"); -//! } //! } //! ``` //! +use serde_json::Value; +use std::collections::HashMap; + use crate::{ input::layout::{SplitDirection, SplitSize, TiledPaneLayout}, - pane_size::{Dimension, PaneGeom}, + pane_size::{Constraint, Dimension, PaneGeom}, }; -use std::collections::HashMap; const INDENT: &str = " "; -pub fn tabs_to_kdl(tabs: &[(String, Vec)]) -> String { - let tab_n = tabs.len(); - let mut kdl_layout = format!("layout {{\n"); - - for (name, panes) in tabs { - // log::info!("PANES in tab:{panes:?}"); - let mut kdl_tab = geoms_to_kdl_tab(&name, &panes, tab_n); - kdl_tab.push_str("\n") +/// Copied from textwrap::indent +fn indent(s: &str, prefix: &str) -> String { + let mut result = String::new(); + for line in s.lines() { + if line.chars().any(|c| !c.is_whitespace()) { + result.push_str(prefix); + result.push_str(line); + } + result.push('\n'); } + result +} - kdl_layout.push_str("}"); - - kdl_layout +fn kdl_string_from_panegeoms(geoms: &Vec) -> String { + let mut kdl_string = String::from("layout {\n"); + let layout = get_layout_from_panegeoms(&geoms, None); + let tab = if &layout.children_split_direction != &SplitDirection::default() { + vec![layout] + } else { + layout.children + }; + kdl_string.push_str(&indent(&kdl_string_from_tab(&tab), INDENT)); + kdl_string.push_str("}"); + kdl_string } /// /// Expects this input /// -/// r#"[ {"x": 0, "y": 0, "cols": 100, "rows": 50}, {"x": 0, "y": 50, "rows": 50, "cols": 50}, {"x": 50, "y": 50, "rows": 50, "cols": 50} ]"# +/// r#"{ "x": 0, "y": 1, "rows": { "constraint": "Percent(100.0)", "inner": 43 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, /// -pub fn parse_geoms_from_json(geoms: &str) -> Vec { - let vec_hashmap_geoms: Vec> = serde_json::from_str(geoms).unwrap(); - vec_hashmap_geoms - .iter() - .map(panegeom_from_hashmap) - .collect() -} - -pub fn panegeom_from_hashmap(map: &HashMap) -> PaneGeom { +fn parse_panegeom_from_json(data_str: &str) -> PaneGeom { + let data: HashMap = serde_json::from_str(data_str).unwrap(); PaneGeom { - x: map["x"] as usize, - y: map["y"] as usize, - rows: Dimension::fixed(map["rows"] as usize), - cols: Dimension::fixed(map["cols"] as usize), - is_stacked: false, + x: data["x"].to_string().parse().unwrap(), + y: data["y"].to_string().parse().unwrap(), + rows: get_dim(&data["rows"]), + cols: get_dim(&data["cols"]), + is_stacked: data["is_stacked"].to_string().parse().unwrap(), } } -/// Tab declaration -fn geoms_to_kdl_tab(name: &str, geoms: &[PaneGeom], tab_n: usize) -> String { - let layout = layout_from_geoms(geoms, None); - // log::info!("TiledLayout: {layout:?}"); - let tab = if &layout.children_split_direction != &SplitDirection::default() { - vec![layout] +fn get_dim(dim_hm: &Value) -> Dimension { + let constr_str = dim_hm["constraint"].to_string(); + let dim = if constr_str.contains("Fixed") { + let value = &constr_str[7..constr_str.len() - 2]; + Dimension::fixed(value.parse().unwrap()) + } else if constr_str.contains("Percent") { + let value = &constr_str[9..constr_str.len() - 2]; + let mut dim = Dimension::percent(value.parse().unwrap()); + dim.set_inner(dim_hm["inner"].to_string().parse().unwrap()); + dim } else { - layout.children + panic!("Constraint is nor a percent nor fixed"); }; + dim +} - // skip tab decl if the tab is the only one - let mut kdl_string = match tab_n { - 1 => format!(""), - _ => format!("tab name=\"{name}\" {{\n"), - }; +// /// Tab declaration +// fn geoms_to_kdl_tab(name: &str, geoms: &[PaneGeom], tab_n: usize) -> String { +// let layout = get_layout_from_panegeoms(geoms, None); +// // log::info!("TiledLayout: {layout:?}"); +// let tab = if &layout.children_split_direction != &SplitDirection::default() { +// vec![layout] +// } else { +// layout.children +// }; - let indent_level = 1; - for layout in tab { - kdl_string.push_str(&kdl_string_from_layout(&layout, indent_level)); - } +// // skip tab decl if the tab is the only one +// let mut kdl_string = match tab_n { +// 1 => format!(""), +// _ => format!("tab name=\"{name}\" {{\n"), +// }; - // skip tab closing } if the tab is the only one - match tab_n { - 1 => {}, - _ => kdl_string.push_str("}"), - }; +// for layout in tab { +// kdl_string.push_str(&kdl_string_from_layout(&layout)); +// } + +// // skip tab closing } if the tab is the only one +// match tab_n { +// 1 => {}, +// _ => kdl_string.push_str("}"), +// }; +// kdl_string +// } + +/// Redundant with `geoms_to_kdl_tab` +fn kdl_string_from_tab(tab: &Vec) -> String { + let mut kdl_string = String::from("tab {\n"); + for layout in tab { + let sub_kdl_string = kdl_string_from_layout(&layout); + kdl_string.push_str(&indent(&sub_kdl_string, INDENT)); + } + kdl_string.push_str("}\n"); kdl_string } /// Pane declaration and recursion -fn kdl_string_from_layout(layout: &TiledPaneLayout, indent_level: usize) -> String { - let mut kdl_string = String::from(&INDENT.repeat(indent_level)); - kdl_string.push_str("pane "); +fn kdl_string_from_layout(layout: &TiledPaneLayout) -> String { + let mut kdl_string = String::from("pane"); match layout.split_size { - Some(SplitSize::Fixed(size)) => kdl_string.push_str(&format!("size={size} ")), - Some(SplitSize::Percent(size)) => kdl_string.push_str(&format!("size={size}% ")), + Some(SplitSize::Fixed(size)) => kdl_string.push_str(&format!(" size={size}")), + Some(SplitSize::Percent(size)) => kdl_string.push_str(&format!(" size=\"{size}%\"")), None => (), }; if layout.children_split_direction != SplitDirection::default() { @@ -124,24 +128,27 @@ fn kdl_string_from_layout(layout: &TiledPaneLayout, indent_level: usize) -> Stri SplitDirection::Horizontal => "horizontal", SplitDirection::Vertical => "vertical", }; - kdl_string.push_str(&format!("split_direction=\"{direction}\" ")); + kdl_string.push_str(&format!(" split_direction=\"{direction}\"")); } if layout.children.is_empty() { kdl_string.push_str("\n"); } else { - kdl_string.push_str("{\n"); + kdl_string.push_str(" {\n"); for pane in &layout.children { - kdl_string.push_str(&kdl_string_from_layout(&pane, indent_level + 1)); + let sub_kdl_string = kdl_string_from_layout(&pane); + kdl_string.push_str(&indent(&sub_kdl_string, INDENT)); } - kdl_string.push_str(&INDENT.repeat(indent_level)); kdl_string.push_str("}\n"); } kdl_string } /// Tab-level parsing -fn layout_from_geoms(geoms: &[PaneGeom], split_size: Option) -> TiledPaneLayout { - let (children_split_direction, splits) = match splits(&geoms) { +fn get_layout_from_panegeoms( + geoms: &Vec, + split_size: Option, +) -> TiledPaneLayout { + let (children_split_direction, splits) = match get_splits(&geoms) { Some(x) => x, None => { return TiledPaneLayout { @@ -150,34 +157,32 @@ fn layout_from_geoms(geoms: &[PaneGeom], split_size: Option) -> Tiled } }, }; - log::info!("SPLITS: {splits:?}"); - - let mut remaining_geoms = geoms.to_owned(); - let children = match splits { - splits if splits.len() == 1 => { - eprintln!("HERE"); - vec![layout_from_subgeoms( - &mut remaining_geoms, - children_split_direction, - (0, splits[0]), - )] - }, - _ => { - eprintln!("OR HERE"); - - (1..splits.len()) + let mut children = Vec::new(); + let mut remaining_geoms = geoms.clone(); + let mut new_geoms = Vec::new(); + let mut new_constraints = Vec::new(); + for i in 1..splits.len() { + let (v_min, v_max) = (splits[i - 1], splits[i]); + let subgeoms: Vec; + (subgeoms, remaining_geoms) = match children_split_direction { + SplitDirection::Horizontal => remaining_geoms + .clone() .into_iter() - .map(|i| { - let (v_min, v_max) = (splits[i - 1], splits[i]); - layout_from_subgeoms( - &mut remaining_geoms, - children_split_direction, - (v_min, v_max), - ) - }) - .collect() - }, - }; + .partition(|g| g.y + g.rows.as_usize() <= v_max), + SplitDirection::Vertical => remaining_geoms + .clone() + .into_iter() + .partition(|g| g.x + g.cols.as_usize() <= v_max), + }; + let constraint = + get_domain_constraint(&subgeoms, &children_split_direction, (v_min, v_max)); + new_geoms.push(subgeoms); + new_constraints.push(constraint); + } + let new_split_sizes = get_split_sizes(&new_constraints); + for (subgeoms, subsplit_size) in new_geoms.iter().zip(new_split_sizes) { + children.push(get_layout_from_panegeoms(&subgeoms, subsplit_size)); + } TiledPaneLayout { children_split_direction, split_size, @@ -186,191 +191,333 @@ fn layout_from_geoms(geoms: &[PaneGeom], split_size: Option) -> Tiled } } -fn layout_from_subgeoms( - remaining_geoms: &mut Vec, - split_direction: SplitDirection, - (v_min, v_max): (usize, usize), -) -> TiledPaneLayout { - let subgeoms: Vec; - (subgeoms, *remaining_geoms) = match split_direction { - SplitDirection::Horizontal => remaining_geoms - .clone() - .into_iter() - .partition(|g| g.y + g.rows.as_usize() <= v_max), - SplitDirection::Vertical => remaining_geoms - .clone() - .into_iter() - .partition(|g| g.x + g.cols.as_usize() <= v_max), - }; - let subsplit_size = SplitSize::Fixed(v_max - v_min); - layout_from_geoms(&subgeoms, Some(subsplit_size)) -} - -fn x_lims(geoms: &[PaneGeom]) -> Option<(usize, usize)> { - let x_min = geoms.iter().map(|g| g.x).min(); - let x_max = geoms.iter().map(|g| g.x + g.rows.as_usize()).max(); - match (x_min, x_max) { +fn get_x_lims(geoms: &Vec) -> Option<(usize, usize)> { + match ( + geoms.iter().map(|g| g.x).min(), + geoms.iter().map(|g| g.x + g.cols.as_usize()).max(), + ) { (Some(x_min), Some(x_max)) => Some((x_min, x_max)), _ => None, } } -fn y_lims(geoms: &[PaneGeom]) -> Option<(usize, usize)> { - let y_min = geoms.iter().map(|g| g.y).min(); - let y_max = geoms.iter().map(|g| g.y + g.rows.as_usize()).max(); - match (y_min, y_max) { +fn get_y_lims(geoms: &Vec) -> Option<(usize, usize)> { + match ( + geoms.iter().map(|g| g.y).min(), + geoms.iter().map(|g| g.y + g.rows.as_usize()).max(), + ) { (Some(y_min), Some(y_max)) => Some((y_min, y_max)), _ => None, } } -fn splits(geoms: &[PaneGeom]) -> Option<(SplitDirection, Vec)> { - log::info!("len: {}", geoms.len()); +/// Returns the `SplitDirection` as well as the values, on the axis +/// perpendicular the `SplitDirection`, for which there is a split spanning +/// the max_cols or max_rows of the domain. The values are ordered +/// increasingly and contains the boundaries of the domain. +fn get_splits(geoms: &Vec) -> Option<(SplitDirection, Vec)> { if geoms.len() == 1 { return None; } - let (x_lims, y_lims) = match (x_lims(&geoms), y_lims(&geoms)) { + let (x_lims, y_lims) = match (get_x_lims(&geoms), get_y_lims(&geoms)) { (Some(x_lims), Some(y_lims)) => (x_lims, y_lims), _ => return None, }; let mut direction = SplitDirection::default(); let mut splits = match direction { - SplitDirection::Vertical => vertical_splits(&geoms, x_lims, y_lims), - SplitDirection::Horizontal => horizontal_splits(&geoms, x_lims, y_lims), + SplitDirection::Vertical => get_col_splits(&geoms, &x_lims, &y_lims), + SplitDirection::Horizontal => get_row_splits(&geoms, &x_lims, &y_lims), }; - log::info!("initial splits: {splits:?}, direction: {direction:?}"); if splits.len() <= 2 { + // ie only the boundaries are present and no real split has been found direction = !direction; splits = match direction { - SplitDirection::Vertical => vertical_splits(&geoms, x_lims, y_lims), - SplitDirection::Horizontal => horizontal_splits(&geoms, x_lims, y_lims), + SplitDirection::Vertical => get_col_splits(&geoms, &x_lims, &y_lims), + SplitDirection::Horizontal => get_row_splits(&geoms, &x_lims, &y_lims), }; } - log::info!("second step splits: {splits:?}"); if splits.len() <= 2 { + // ie no real split has been found in both directions None } else { Some((direction, splits)) } } -fn vertical_splits( - geoms: &[PaneGeom], - x_lims: (usize, usize), - y_lims: (usize, usize), +/// Returns a vector containing the abscisse (x) of the cols that split the +/// domain including the boundaries, ie the min and max abscisse values. +fn get_col_splits( + geoms: &Vec, + (_, x_max): &(usize, usize), + (y_min, y_max): &(usize, usize), ) -> Vec { - log::info!("VERTICAL:{}", geoms.len()); - let ((_, x_max), (y_min, y_max)) = (x_lims, y_lims); - let height = y_max - y_min; + let max_rows = y_max - y_min; let mut splits = Vec::new(); - for x in geoms.iter().map(|g| g.x) { + let mut sorted_geoms = geoms.clone(); + sorted_geoms.sort_by_key(|g| g.x); + for x in sorted_geoms.iter().map(|g| g.x) { if splits.contains(&x) { continue; } - if geoms + if sorted_geoms .iter() .filter(|g| g.x == x) .map(|g| g.rows.as_usize()) .sum::() - == height + == max_rows { splits.push(x); }; } - splits.push(x_max); + splits.push(*x_max); // Necessary as `g.x` is from the upper-left corner splits } -fn horizontal_splits( - geoms: &[PaneGeom], - x_lims: (usize, usize), - y_lims: (usize, usize), +/// Returns a vector containing the coordinate (y) of the rows that split the +/// domain including the boundaries, ie the min and max coordinate values. +fn get_row_splits( + geoms: &Vec, + (x_min, x_max): &(usize, usize), + (_, y_max): &(usize, usize), ) -> Vec { - log::info!("HORIZONTAL:{}", geoms.len()); - let ((x_min, x_max), (_, y_max)) = (x_lims, y_lims); - let width = x_max - x_min; + let max_cols = x_max - x_min; let mut splits = Vec::new(); - for y in geoms.iter().map(|g| g.y) { + let mut sorted_geoms = geoms.clone(); + sorted_geoms.sort_by_key(|g| g.y); + for y in sorted_geoms.iter().map(|g| g.y) { if splits.contains(&y) { continue; } - if geoms + if sorted_geoms .iter() .filter(|g| g.y == y) .map(|g| g.cols.as_usize()) .sum::() - == width + == max_cols { splits.push(y); }; } - splits.push(y_max); + splits.push(*y_max); // Necessary as `g.y` is from the upper-left corner splits } +/// Get the constraint of the domain considered, base on the rows or columns, +/// depending on the split direction provided. +fn get_domain_constraint( + geoms: &Vec, + split_direction: &SplitDirection, + (v_min, v_max): (usize, usize), +) -> Constraint { + match split_direction { + SplitDirection::Horizontal => get_domain_row_constraint(&geoms, (v_min, v_max)), + SplitDirection::Vertical => get_domain_col_constraint(&geoms, (v_min, v_max)), + } +} + +fn get_domain_col_constraint(geoms: &Vec, (x_min, x_max): (usize, usize)) -> Constraint { + let mut percent = 0.0; + let mut x = x_min; + while x != x_max { + // we only look at one (ie the last) geom that has value `x` for `g.x` + let geom = geoms.iter().filter(|g| g.x == x).last().unwrap(); + if let Some(size) = geom.cols.as_percent() { + percent += size; + } + x += geom.cols.as_usize(); + } + if percent == 0.0 { + Constraint::Fixed(x_max - x_min) + } else { + Constraint::Percent(percent) + } +} + +fn get_domain_row_constraint(geoms: &Vec, (y_min, y_max): (usize, usize)) -> Constraint { + let mut percent = 0.0; + let mut y = y_min; + while y != y_max { + // we only look at one (ie the last) geom that has value `y` for `g.y` + let geom = geoms.iter().filter(|g| g.y == y).last().unwrap(); + if let Some(size) = geom.rows.as_percent() { + percent += size; + } + y += geom.rows.as_usize(); + } + if percent == 0.0 { + Constraint::Fixed(y_max - y_min) + } else { + Constraint::Percent(percent) + } +} + +/// Returns split sizes for all the children of a `TiledPaneLayout` based on +/// their constraints. +fn get_split_sizes(constraints: &Vec) -> Vec> { + let mut split_sizes = Vec::new(); + let max_percent = constraints + .iter() + .filter_map(|c| match c { + Constraint::Percent(size) => Some(size), + _ => None, + }) + .sum::(); + for constraint in constraints { + let split_size = match constraint { + Constraint::Fixed(size) => Some(SplitSize::Fixed(*size)), + Constraint::Percent(size) => { + if size == &max_percent { + None + } else { + Some(SplitSize::Percent((100.0 * size / max_percent) as usize)) + } + }, + }; + split_sizes.push(split_size); + } + split_sizes +} + #[cfg(test)] mod tests { use super::*; use expect_test::expect; - - const LAYOUT: &[&str] = &[ - r#"[ {"x": 0, "y": 0, "cols": 100, "rows": 50}, {"x": 0, "y": 50, "rows": 50, "cols": 50}, {"x": 50, "y": 50, "rows": 50, "cols": 50} ]"#, - r#"[{"x": 0, "y": 0, "cols": 80, "rows": 30}, {"x": 0, "y": 30, "rows": 30, "cols": 30}, {"x": 30, "y": 30, "rows": 30, "cols": 50}]"#, - r#"[{"x": 0, "y": 0, "cols": 60, "rows": 40}, {"x": 60, "y": 0, "rows": 40, "cols": 20}, {"x": 0, "y": 40, "rows": 20, "cols": 60}, {"x": 60, "y": 40, "rows": 20, "cols": 20}]"#, - r#"[{"x": 0, "y": 0, "cols": 40, "rows": 20}, {"x": 40, "y": 0, "rows": 20, "cols": 40}, {"x": 0, "y": 20, "rows": 20, "cols": 25}, {"x": 25, "y": 20, "rows": 20, "cols": 30}, {"x": 55, "y": 20, "rows": 20, "cols": 25}, {"x": 0, "y": 40, "rows": 20, "cols": 40}, {"x": 40, "y": 40, "rows": 20, "cols": 40}]"#, - r#"[{"x": 0, "y": 0, "cols": 40, "rows": 30}, {"x": 0, "y": 30, "cols": 40, "rows": 30}, {"x": 40, "y": 0, "cols": 40, "rows":20}, {"x": 40, "y": 20, "cols": 20, "rows": 20}, {"x": 60, "y": 20, "cols": 20, "rows": 20}, {"x": 40, "y": 40, "cols": 40, "rows": 20}]"#, - r#"[{"x": 0, "y": 0, "cols": 30, "rows": 20}, {"x": 0, "y": 20, "cols": 30, "rows": 20}, {"x": 0, "y": 40, "cols": 30, "rows": 10}, {"x": 30, "y": 0, "cols": 30, "rows": 50}, {"x": 0, "y": 50, "cols": 60, "rows": 10}, {"x": 60, "y": 0, "cols": 20, "rows": 60}]"#, + const PANEGEOMS_JSON: &[&[&str]] = &[ + &[ + r#"{ "x": 0, "y": 1, "rows": { "constraint": "Percent(100.0)", "inner": 43 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, + r#"{ "x": 0, "y": 0, "rows": { "constraint": "Fixed(1)", "inner": 1 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, + r#"{ "x": 0, "y": 44, "rows": { "constraint": "Fixed(2)", "inner": 2 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, + ], + &[ + r#"{ "x": 0, "y": 0, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, + r#"{ "x": 0, "y": 26, "rows": { "constraint": "Fixed(20)", "inner": 20 }, "cols": { "constraint": "Fixed(50)", "inner": 50 }, "is_stacked": false }"#, + r#"{ "x": 50, "y": 26, "rows": { "constraint": "Fixed(20)", "inner": 20 }, "cols": { "constraint": "Percent(100.0)", "inner": 161 }, "is_stacked": false }"#, + ], + &[ + r#"{ "x": 0, "y": 0, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 106 }, "is_stacked": false }"#, + r#"{ "x": 106, "y": 0, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 105 }, "is_stacked": false }"#, + r#"{ "x": 0, "y": 10, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Fixed(40)", "inner": 40 }, "is_stacked": false }"#, + r#"{ "x": 40, "y": 10, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Percent(100.0)", "inner": 131 }, "is_stacked": false }"#, + r#"{ "x": 171, "y": 10, "rows": { "constraint": "Percent(100.0)", "inner": 26 }, "cols": { "constraint": "Fixed(40)", "inner": 40 }, "is_stacked": false }"#, + r#"{ "x": 0, "y": 36, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 106 }, "is_stacked": false }"#, + r#"{ "x": 106, "y": 36, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(50.0)", "inner": 105 }, "is_stacked": false }"#, + ], + &[ + r#"{ "x": 0, "y": 0, "rows": { "constraint": "Percent(30.0)", "inner": 11 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#, + r#"{ "x": 0, "y": 11, "rows": { "constraint": "Percent(30.0)", "inner": 11 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#, + r#"{ "x": 0, "y": 22, "rows": { "constraint": "Percent(40.0)", "inner": 14 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#, + r#"{ "x": 74, "y": 0, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Percent(35.0)", "inner": 74 }, "is_stacked": false }"#, + r#"{ "x": 0, "y": 36, "rows": { "constraint": "Fixed(10)", "inner": 10 }, "cols": { "constraint": "Percent(70.0)", "inner": 148 }, "is_stacked": false }"#, + r#"{ "x": 148, "y": 0, "rows": { "constraint": "Percent(100.0)", "inner": 46 }, "cols": { "constraint": "Percent(30.0)", "inner": 63 }, "is_stacked": false }"#, + ], + &[ + r#"{ "x": 0, "y": 0, "rows": { "constraint": "Fixed(5)", "inner": 5 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, + r#"{ "x": 0, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Fixed(20)", "inner": 20 }, "is_stacked": false }"#, + r#"{ "x": 20, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Percent(50.0)", "inner": 86 }, "is_stacked": false }"#, + r#"{ "x": 106, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Percent(50.0)", "inner": 85 }, "is_stacked": false }"#, + r#"{ "x": 191, "y": 5, "rows": { "constraint": "Percent(100.0)", "inner": 36 }, "cols": { "constraint": "Fixed(20)", "inner": 20 }, "is_stacked": false }"#, + r#"{ "x": 0, "y": 41, "rows": { "constraint": "Fixed(5)", "inner": 5 }, "cols": { "constraint": "Percent(100.0)", "inner": 211 }, "is_stacked": false }"#, + ], ]; - const DIM: Dimension = Dimension { - constraint: crate::pane_size::Constraint::Fixed(5), - inner: 0, - }; - const PANE_GEOM: PaneGeom = PaneGeom { - x: 0, - y: 0, - rows: DIM, - cols: DIM, - is_stacked: false, - }; #[test] fn geoms() { - let geoms = parse_geoms_from_json(LAYOUT[0]); - let kdl = geoms_to_kdl_tab("test", &geoms, 2); - expect![[r#" - "tab name=\"test\" {\n pane size=50 \n pane size=50 split_direction=\"vertical\" {\n pane size=50 \n pane size=50 \n }\n}" - "#]].assert_debug_eq(&kdl); - - let geoms = parse_geoms_from_json(LAYOUT[1]); - let kdl = geoms_to_kdl_tab("test", &geoms, 1); - expect![[r#" - "" - "#]] + let geoms = PANEGEOMS_JSON[0] + .iter() + .map(|pg| parse_panegeom_from_json(pg)) + .collect(); + let kdl = kdl_string_from_panegeoms(&geoms); + expect![[r#"layout { + tab { + pane size=1 + pane + pane size=2 + } +}"#]] .assert_debug_eq(&kdl); - let geoms = parse_geoms_from_json(LAYOUT[2]); - let kdl = geoms_to_kdl_tab("test", &geoms, 1); - expect![[r#" - " pane split_direction=\"vertical\" {\n pane size=60 \n pane size=40 \n }\n" - "#]].assert_debug_eq(&kdl); + let geoms = PANEGEOMS_JSON[1] + .iter() + .map(|pg| parse_panegeom_from_json(pg)) + .collect(); + let kdl = kdl_string_from_panegeoms(&geoms); + expect![[r#"layout { + tab { + pane + pane size=20 split_direction="vertical" { + pane size=50 + pane + } + } +}"#]] + .assert_debug_eq(&kdl); - let geoms = parse_geoms_from_json(LAYOUT[3]); - let kdl = geoms_to_kdl_tab("test", &geoms, 1); - expect![[r#" - "" - "#]] + let geoms = PANEGEOMS_JSON[2] + .iter() + .map(|pg| parse_panegeom_from_json(pg)) + .collect(); + let kdl = kdl_string_from_panegeoms(&geoms); + expect![[r#"layout { + tab { + pane size=10 split_direction="vertical" { + pane size="50%" + pane size="50%" + } + pane split_direction="vertical" { + pane size=40 + pane + pane size=40 + } + pane size=10 split_direction="vertical" { + pane size="50%" + pane size="50%" + } + } +}"#]] .assert_debug_eq(&kdl); - let geoms = parse_geoms_from_json(LAYOUT[4]); - let kdl = geoms_to_kdl_tab("test", &geoms, 1); - expect![[r#" - " pane split_direction=\"vertical\" {\n pane size=40 \n pane size=40 {\n pane size=20 \n pane size=20 split_direction=\"vertical\" {\n pane size=20 \n pane size=20 \n }\n pane size=20 \n }\n }\n" - "#]].assert_debug_eq(&kdl); + let geoms = PANEGEOMS_JSON[3] + .iter() + .map(|pg| parse_panegeom_from_json(pg)) + .collect(); + let kdl = kdl_string_from_panegeoms(&geoms); + expect![[r#"layout { + tab { + pane split_direction="vertical" { + pane size="70%" { + pane split_direction="vertical" { + pane size="50%" { + pane size="30%" + pane size="30%" + pane size="40%" + } + pane size="50%" + } + pane size=10 + } + pane size="30%" + } + } +}"#]] + .assert_debug_eq(&kdl); - let geoms = parse_geoms_from_json(LAYOUT[5]); - let kdl = geoms_to_kdl_tab("test", &geoms, 1); - expect![[r#" - " pane split_direction=\"vertical\" {\n pane size=60 \n pane size=60 \n }\n" - "#]].assert_debug_eq(&kdl); + let geoms = PANEGEOMS_JSON[4] + .iter() + .map(|pg| parse_panegeom_from_json(pg)) + .collect(); + let kdl = kdl_string_from_panegeoms(&geoms); + expect![[r#"layout { + tab { + pane size=5 + pane split_direction="vertical" { + pane size=20 + pane size="50%" + pane size="50%" + pane size=20 + } + pane size=5 + } +}"#]] + .assert_debug_eq(&kdl); } } From 09d36fe4650d6b3e499de9a747ec30e6f533f73c Mon Sep 17 00:00:00 2001 From: Thomas Linford Date: Fri, 30 Jun 2023 09:42:23 +0200 Subject: [PATCH 008/128] fix(tab-bar,compact-bar): tab switching with mouse sometimes not working (#2587) * tab-bar: fix clicks sometimes not registering Caching the click position wasn't working across multiple plugin instances. Also a couple of refactors: - move the code with the tab switching logic inside update - avoid rendering when calling switch_tab_to, since it will happen anyway afterwards * same fix for compact-bar --- default-plugins/compact-bar/src/main.rs | 40 ++++++++---------------- default-plugins/compact-bar/src/tab.rs | 29 +++++++++++++++++ default-plugins/tab-bar/src/main.rs | 41 +++++++++---------------- default-plugins/tab-bar/src/tab.rs | 29 +++++++++++++++++ 4 files changed, 84 insertions(+), 55 deletions(-) diff --git a/default-plugins/compact-bar/src/main.rs b/default-plugins/compact-bar/src/main.rs index 451c3cd8f5..40fa65f52a 100644 --- a/default-plugins/compact-bar/src/main.rs +++ b/default-plugins/compact-bar/src/main.rs @@ -4,6 +4,7 @@ mod tab; use std::cmp::{max, min}; use std::convert::TryInto; +use tab::get_tab_to_focus; use zellij_tile::prelude::*; use crate::line::tab_line; @@ -21,8 +22,7 @@ struct State { tabs: Vec, active_tab_idx: usize, mode_info: ModeInfo, - mouse_click_pos: usize, - should_change_tab: bool, + tab_line: Vec, } static ARROW_SEPARATOR: &str = ""; @@ -63,18 +63,15 @@ impl ZellijPlugin for State { }, Event::Mouse(me) => match me { Mouse::LeftClick(_, col) => { - if self.mouse_click_pos != col { - should_render = true; - self.should_change_tab = true; + let tab_to_focus = get_tab_to_focus(&self.tab_line, self.active_tab_idx, col); + if let Some(idx) = tab_to_focus { + switch_tab_to(idx.try_into().unwrap()); } - self.mouse_click_pos = col; }, Mouse::ScrollUp(_) => { - should_render = true; switch_tab_to(min(self.active_tab_idx + 1, self.tabs.len()) as u32); }, Mouse::ScrollDown(_) => { - should_render = true; switch_tab_to(max(self.active_tab_idx.saturating_sub(1), 1) as u32); }, _ => {}, @@ -117,7 +114,7 @@ impl ZellijPlugin for State { is_alternate_tab = !is_alternate_tab; all_tabs.push(tab); } - let tab_line = tab_line( + self.tab_line = tab_line( self.mode_info.session_name.as_deref(), all_tabs, active_tab_index, @@ -129,34 +126,21 @@ impl ZellijPlugin for State { &active_swap_layout_name, is_swap_layout_dirty, ); - let mut s = String::new(); - let mut len_cnt = 0; - for bar_part in tab_line { - s = format!("{}{}", s, &bar_part.part); - - if self.should_change_tab - && self.mouse_click_pos >= len_cnt - && self.mouse_click_pos < len_cnt + bar_part.len - && bar_part.tab_index.is_some() - { - // Tabs are indexed starting from 1, therefore we need add 1 to tab_index. - let tab_index: u32 = bar_part.tab_index.unwrap().try_into().unwrap(); - switch_tab_to(tab_index + 1); - } - len_cnt += bar_part.len; - } + let output = self + .tab_line + .iter() + .fold(String::new(), |output, part| output + &part.part); let background = match self.mode_info.style.colors.theme_hue { ThemeHue::Dark => self.mode_info.style.colors.black, ThemeHue::Light => self.mode_info.style.colors.white, }; match background { PaletteColor::Rgb((r, g, b)) => { - print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", s, r, g, b); + print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", output, r, g, b); }, PaletteColor::EightBit(color) => { - print!("{}\u{1b}[48;5;{}m\u{1b}[0K", s, color); + print!("{}\u{1b}[48;5;{}m\u{1b}[0K", output, color); }, } - self.should_change_tab = false; } } diff --git a/default-plugins/compact-bar/src/tab.rs b/default-plugins/compact-bar/src/tab.rs index f6776b7382..1aa13d4c33 100644 --- a/default-plugins/compact-bar/src/tab.rs +++ b/default-plugins/compact-bar/src/tab.rs @@ -99,3 +99,32 @@ pub fn tab_style( render_tab(tabname, tab, is_alternate_tab, palette, separator) } + +pub(crate) fn get_tab_to_focus( + tab_line: &[LinePart], + active_tab_idx: usize, + mouse_click_col: usize, +) -> Option { + let clicked_line_part = get_clicked_line_part(tab_line, mouse_click_col)?; + let clicked_tab_idx = clicked_line_part.tab_index?; + // tabs are indexed starting from 1 so we need to add 1 + let clicked_tab_idx = clicked_tab_idx + 1; + if clicked_tab_idx != active_tab_idx { + return Some(clicked_tab_idx); + } + None +} + +pub(crate) fn get_clicked_line_part( + tab_line: &[LinePart], + mouse_click_col: usize, +) -> Option<&LinePart> { + let mut len = 0; + for tab_line_part in tab_line { + if mouse_click_col >= len && mouse_click_col < len + tab_line_part.len { + return Some(tab_line_part); + } + len += tab_line_part.len; + } + None +} diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs index 2df6a13dfb..05fdd5608d 100644 --- a/default-plugins/tab-bar/src/main.rs +++ b/default-plugins/tab-bar/src/main.rs @@ -4,6 +4,7 @@ mod tab; use std::cmp::{max, min}; use std::convert::TryInto; +use tab::get_tab_to_focus; use zellij_tile::prelude::*; use crate::line::tab_line; @@ -21,8 +22,7 @@ struct State { tabs: Vec, active_tab_idx: usize, mode_info: ModeInfo, - mouse_click_pos: usize, - should_change_tab: bool, + tab_line: Vec, } static ARROW_SEPARATOR: &str = ""; @@ -52,6 +52,7 @@ impl ZellijPlugin for State { if let Some(active_tab_index) = tabs.iter().position(|t| t.active) { // tabs are indexed starting from 1 so we need to add 1 let active_tab_idx = active_tab_index + 1; + if self.active_tab_idx != active_tab_idx || self.tabs != tabs { should_render = true; } @@ -63,18 +64,15 @@ impl ZellijPlugin for State { }, Event::Mouse(me) => match me { Mouse::LeftClick(_, col) => { - if self.mouse_click_pos != col { - should_render = true; - self.should_change_tab = true; + let tab_to_focus = get_tab_to_focus(&self.tab_line, self.active_tab_idx, col); + if let Some(idx) = tab_to_focus { + switch_tab_to(idx.try_into().unwrap()); } - self.mouse_click_pos = col; }, Mouse::ScrollUp(_) => { - should_render = true; switch_tab_to(min(self.active_tab_idx + 1, self.tabs.len()) as u32); }, Mouse::ScrollDown(_) => { - should_render = true; switch_tab_to(max(self.active_tab_idx.saturating_sub(1), 1) as u32); }, _ => {}, @@ -113,7 +111,7 @@ impl ZellijPlugin for State { is_alternate_tab = !is_alternate_tab; all_tabs.push(tab); } - let tab_line = tab_line( + self.tab_line = tab_line( self.mode_info.session_name.as_deref(), all_tabs, active_tab_index, @@ -122,34 +120,23 @@ impl ZellijPlugin for State { self.mode_info.capabilities, self.mode_info.style.hide_session_name, ); - let mut s = String::new(); - let mut len_cnt = 0; - for bar_part in tab_line { - s = format!("{}{}", s, &bar_part.part); - if self.should_change_tab - && self.mouse_click_pos >= len_cnt - && self.mouse_click_pos < len_cnt + bar_part.len - && bar_part.tab_index.is_some() - { - // Tabs are indexed starting from 1, therefore we need add 1 to tab_index. - let tab_index: u32 = bar_part.tab_index.unwrap().try_into().unwrap(); - switch_tab_to(tab_index + 1); - } - len_cnt += bar_part.len; - } + let output = self + .tab_line + .iter() + .fold(String::new(), |output, part| output + &part.part); + let background = match self.mode_info.style.colors.theme_hue { ThemeHue::Dark => self.mode_info.style.colors.black, ThemeHue::Light => self.mode_info.style.colors.white, }; match background { PaletteColor::Rgb((r, g, b)) => { - print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", s, r, g, b); + print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", output, r, g, b); }, PaletteColor::EightBit(color) => { - print!("{}\u{1b}[48;5;{}m\u{1b}[0K", s, color); + print!("{}\u{1b}[48;5;{}m\u{1b}[0K", output, color); }, } - self.should_change_tab = false; } } diff --git a/default-plugins/tab-bar/src/tab.rs b/default-plugins/tab-bar/src/tab.rs index 82e8ad1949..4b2ae7776e 100644 --- a/default-plugins/tab-bar/src/tab.rs +++ b/default-plugins/tab-bar/src/tab.rs @@ -99,3 +99,32 @@ pub fn tab_style( render_tab(tabname, tab, is_alternate_tab, palette, separator) } + +pub(crate) fn get_tab_to_focus( + tab_line: &[LinePart], + active_tab_idx: usize, + mouse_click_col: usize, +) -> Option { + let clicked_line_part = get_clicked_line_part(tab_line, mouse_click_col)?; + let clicked_tab_idx = clicked_line_part.tab_index?; + // tabs are indexed starting from 1 so we need to add 1 + let clicked_tab_idx = clicked_tab_idx + 1; + if clicked_tab_idx != active_tab_idx { + return Some(clicked_tab_idx); + } + None +} + +pub(crate) fn get_clicked_line_part( + tab_line: &[LinePart], + mouse_click_col: usize, +) -> Option<&LinePart> { + let mut len = 0; + for tab_line_part in tab_line { + if mouse_click_col >= len && mouse_click_col < len + tab_line_part.len { + return Some(tab_line_part); + } + len += tab_line_part.len; + } + None +} From 626565390fa31223d4ecfa32b8cb42123b56b88c Mon Sep 17 00:00:00 2001 From: Thomas Linford Date: Fri, 30 Jun 2023 09:44:19 +0200 Subject: [PATCH 009/128] docs(changelog): plugins tab switching with mouse fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abfb784fc7..4a2e20c83b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +* fix(tab-bar,compact-bar): tab switching with mouse sometimes not working (https://github.com/zellij-org/zellij/pull/2587) ## [0.37.2] - 2023-06-20 * hotfix: include theme files into binary (https://github.com/zellij-org/zellij/pull/2566) From b2e6fe685def9c5ea18d6cdf5a77bb879ec9091a Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 12 Jul 2023 11:31:00 +0200 Subject: [PATCH 010/128] feat(ui): new status bar mode (#2619) * supermode prototype * fix integration tests * fix tests * style(fmt): rustfmt --- default-plugins/status-bar/src/first_line.rs | 367 ++++++++++++++++-- default-plugins/status-bar/src/main.rs | 81 +++- default-plugins/status-bar/src/second_line.rs | 140 ++----- .../status-bar/src/tip/data/compact_layout.rs | 2 +- .../src/tip/data/edit_scrollbuffer.rs | 2 +- .../src/tip/data/floating_panes_mouse.rs | 2 +- .../status-bar/src/tip/data/quicknav.rs | 2 +- .../status-bar/src/tip/data/sync_tab.rs | 2 +- src/tests/e2e/cases.rs | 2 +- .../zellij__tests__e2e__cases__lock_mode.snap | 4 +- ...ests__e2e__cases__mirrored_sessions-2.snap | 4 +- ...__e2e__cases__scrolling_inside_a_pane.snap | 4 +- 12 files changed, 439 insertions(+), 173 deletions(-) diff --git a/default-plugins/status-bar/src/first_line.rs b/default-plugins/status-bar/src/first_line.rs index 23a6d5869c..183aa64f99 100644 --- a/default-plugins/status-bar/src/first_line.rs +++ b/default-plugins/status-bar/src/first_line.rs @@ -8,13 +8,14 @@ use crate::{ }; use crate::{ColoredElements, LinePart}; +#[derive(Debug, Clone, Copy)] struct KeyShortcut { mode: KeyMode, - action: KeyAction, + pub action: KeyAction, key: Option, } -#[derive(PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] enum KeyAction { Lock, Pane, @@ -27,6 +28,7 @@ enum KeyAction { Tmux, } +#[derive(Debug, Clone, Copy)] enum KeyMode { Unselected, UnselectedAlternate, @@ -34,6 +36,20 @@ enum KeyMode { Disabled, } +fn letter_shortcut(key: &Key, with_prefix: bool) -> String { + if with_prefix { + format!("{}", key) + } else { + match key { + Key::F(c) => format!("{}", c), + Key::Ctrl(c) => format!("{}", c), + Key::Char(_) => format!("{}", key), + Key::Alt(c) => format!("{}", c), + _ => String::from("??"), + } + } +} + impl KeyShortcut { pub fn new(mode: KeyMode, action: KeyAction, key: Option) -> Self { KeyShortcut { mode, action, key } @@ -57,17 +73,7 @@ impl KeyShortcut { Some(k) => k, None => return String::from("?"), }; - if with_prefix { - format!("{}", key) - } else { - match key { - Key::F(c) => format!("{}", c), - Key::Ctrl(c) => format!("{}", c), - Key::Char(_) => format!("{}", key), - Key::Alt(c) => format!("{}", c), - _ => String::from("??"), - } - } + letter_shortcut(&key, with_prefix) } } @@ -196,6 +202,58 @@ fn short_mode_shortcut( } } +fn short_key_indicators( + max_len: usize, + keys: &[KeyShortcut], + palette: ColoredElements, + separator: &str, + mode_info: &ModeInfo, + no_super: bool, +) -> LinePart { + let mut line_part = if no_super { + LinePart::default() + } else { + superkey(palette, separator, mode_info) + }; + let shared_super = if no_super { true } else { line_part.len > 0 }; + for ctrl_key in keys { + let line_empty = line_part.len == 0; + let key = short_mode_shortcut(ctrl_key, palette, separator, shared_super, line_empty); + line_part.part = format!("{}{}", line_part.part, key.part); + line_part.len += key.len; + } + if line_part.len < max_len { + return line_part; + } + + // Shortened doesn't fit, print nothing + line_part = LinePart::default(); + line_part +} + +fn full_key_indicators( + keys: &[KeyShortcut], + palette: ColoredElements, + separator: &str, + mode_info: &ModeInfo, + no_super: bool, +) -> LinePart { + // Print full-width hints + let mut line_part = if no_super { + LinePart::default() + } else { + superkey(palette, separator, mode_info) + }; + let shared_super = if no_super { true } else { line_part.len > 0 }; + for ctrl_key in keys { + let line_empty = line_part.len == 0; + let key = long_mode_shortcut(ctrl_key, palette, separator, shared_super, line_empty); + line_part.part = format!("{}{}", line_part.part, key.part); + line_part.len += key.len; + } + line_part +} + fn key_indicators( max_len: usize, keys: &[KeyShortcut], @@ -402,6 +460,111 @@ pub fn superkey(palette: ColoredElements, separator: &str, mode_info: &ModeInfo) } } +fn standby_mode_shortcut_key(help: &ModeInfo) -> Key { + let binds = &help.get_mode_keybinds(); + match help.mode { + InputMode::Locked => to_char(action_key( + binds, + &[Action::SwitchToMode(InputMode::Normal)], + )), + _ => to_char(action_key( + binds, + &[Action::SwitchToMode(InputMode::Locked)], + )), + } + .unwrap_or(Key::Char('?')) +} + +fn standby_mode_ui_indication( + has_shared_super: bool, + standby_mode: &InputMode, + standby_mode_shortcut_key: Key, + colored_elements: ColoredElements, + separator: &str, +) -> LinePart { + let mut line_part = LinePart::default(); + let standby_mode_shortcut = standby_mode_single_letter_selected( + has_shared_super, + standby_mode_shortcut_key, + colored_elements, + separator, + ); + // standby mode text hint + let key_shortcut = KeyShortcut::new( + KeyMode::Unselected, + input_mode_to_key_action(&standby_mode), + None, + ); + let styled_text = colored_elements + .unselected + .styled_text + .paint(format!(" {} ", key_shortcut.full_text())); + let suffix_separator = colored_elements + .unselected + .suffix_separator + .paint(separator); + let standby_mode_text = LinePart { + part: ANSIStrings(&[styled_text, suffix_separator]).to_string(), + len: key_shortcut.full_text().chars().count() + separator.chars().count() + 2, // 2 padding + }; + line_part.part = format!("{}{}", line_part.part, standby_mode_shortcut.part); + line_part.len += standby_mode_shortcut.len; + line_part.part = format!("{}{}", line_part.part, standby_mode_text.part); + line_part.len += standby_mode_text.len; + line_part +} + +fn standby_mode_single_letter_unselected( + has_shared_super: bool, + shortcut_key: Key, + palette: ColoredElements, + separator: &str, +) -> LinePart { + let prefix_separator = palette.unselected.prefix_separator.paint(separator); + let char_shortcut = palette.unselected.char_shortcut.paint(format!( + " {} ", + letter_shortcut(&shortcut_key, has_shared_super) + )); + let suffix_separator = palette.unselected.suffix_separator.paint(separator); + let len = prefix_separator.chars().count() + + char_shortcut.chars().count() + + suffix_separator.chars().count(); + LinePart { + part: ANSIStrings(&[prefix_separator, char_shortcut, suffix_separator]).to_string(), + len, + } +} + +fn standby_mode_single_letter_selected( + has_shared_super: bool, + shortcut_key: Key, + palette: ColoredElements, + separator: &str, +) -> LinePart { + let prefix_separator = palette + .selected_standby_shortcut + .prefix_separator + .paint(separator); + let char_shortcut = palette + .selected_standby_shortcut + .char_shortcut + .paint(format!( + " {} ", + letter_shortcut(&shortcut_key, has_shared_super) + )); + let suffix_separator = palette + .selected_standby_shortcut + .suffix_separator + .paint(separator); + let len = prefix_separator.chars().count() + + char_shortcut.chars().count() + + suffix_separator.chars().count(); + LinePart { + part: ANSIStrings(&[prefix_separator, char_shortcut, suffix_separator]).to_string(), + len, + } +} + pub fn to_char(kv: Vec) -> Option { let key = kv .iter() @@ -419,6 +582,19 @@ pub fn to_char(kv: Vec) -> Option { key.cloned() } +fn input_mode_to_key_action(input_mode: &InputMode) -> KeyAction { + match input_mode { + InputMode::Normal | InputMode::Prompt | InputMode::Tmux => KeyAction::Lock, + InputMode::Locked => KeyAction::Lock, + InputMode::Pane | InputMode::RenamePane => KeyAction::Pane, + InputMode::Tab | InputMode::RenameTab => KeyAction::Tab, + InputMode::Resize => KeyAction::Resize, + InputMode::Move => KeyAction::Move, + InputMode::Scroll | InputMode::Search | InputMode::EnterSearch => KeyAction::Search, + InputMode::Session => KeyAction::Session, + } +} + /// Get the [`KeyShortcut`] for a specific [`InputMode`]. /// /// Iterates over the contents of `shortcuts` to find the [`KeyShortcut`] with the [`KeyAction`] @@ -449,7 +625,8 @@ fn get_key_shortcut_for_mode<'a>( None } -pub fn first_line( +pub fn first_line_supermode( + standby_mode: &InputMode, help: &ModeInfo, tab_info: Option<&TabInfo>, max_len: usize, @@ -457,9 +634,129 @@ pub fn first_line( ) -> LinePart { let supports_arrow_fonts = !help.capabilities.arrow_fonts; let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts); + + let standby_mode_shortcut_key = standby_mode_shortcut_key(&help); + + let mut line = superkey(colored_elements, separator, help); + let has_shared_super = line.len == 0; + let max_len_without_superkey = max_len.saturating_sub(line.len); + let mut append_to_line = |line_part_to_append: LinePart| { + line.part = format!("{}{}", line.part, line_part_to_append.part); + line.len += line_part_to_append.len; + }; + match help.mode { + InputMode::Locked => { + let standby_mode_shortcut = standby_mode_single_letter_unselected( + has_shared_super, + standby_mode_shortcut_key, + colored_elements, + separator, + ); + append_to_line(standby_mode_shortcut); + line + }, + _ => { + let mut default_keys = generate_default_keys(help); + default_keys.remove(0); // remove locked mode which is not relevant to the supermode ui + + if let Some(position) = default_keys + .iter() + .position(|d| d.action == input_mode_to_key_action(standby_mode)) + { + let standby_mode_ui_ribbon = standby_mode_ui_indication( + has_shared_super, + &standby_mode, + standby_mode_shortcut_key, + colored_elements, + separator, + ); + let first_keybinds: Vec = + default_keys.iter().cloned().take(position).collect(); + let second_keybinds: Vec = + default_keys.iter().cloned().skip(position + 1).collect(); + let first_key_indicators = + full_key_indicators(&first_keybinds, colored_elements, separator, help, true); + let second_key_indicators = + full_key_indicators(&second_keybinds, colored_elements, separator, help, true); + + if first_key_indicators.len + standby_mode_ui_ribbon.len + second_key_indicators.len + < max_len_without_superkey + { + append_to_line(first_key_indicators); + append_to_line(standby_mode_ui_ribbon); + append_to_line(second_key_indicators); + } else { + let length_of_each_half = + max_len_without_superkey.saturating_sub(standby_mode_ui_ribbon.len) / 2; + let first_key_indicators = short_key_indicators( + length_of_each_half, + &first_keybinds, + colored_elements, + separator, + help, + true, + ); + let second_key_indicators = short_key_indicators( + length_of_each_half, + &second_keybinds, + colored_elements, + separator, + help, + true, + ); + append_to_line(first_key_indicators); + append_to_line(standby_mode_ui_ribbon); + append_to_line(second_key_indicators); + } + if line.len < max_len { + if let Some(tab_info) = tab_info { + let remaining_space = max_len.saturating_sub(line.len); + line.append(&swap_layout_status_and_padding( + &tab_info, + remaining_space, + separator, + colored_elements, + help, + )); + } + } + } + line + }, + } +} + +fn swap_layout_status_and_padding( + tab_info: &TabInfo, + mut remaining_space: usize, + separator: &str, + colored_elements: ColoredElements, + help: &ModeInfo, +) -> LinePart { + let mut line = LinePart::default(); + if let Some(swap_layout_status) = swap_layout_status( + remaining_space, + &tab_info.active_swap_layout_name, + tab_info.is_swap_layout_dirty, + help, + colored_elements, + &help.style.colors, + separator, + ) { + remaining_space -= swap_layout_status.len; + for _ in 0..remaining_space { + line.part + .push_str(&ANSIStrings(&[colored_elements.superkey_prefix.paint(" ")]).to_string()); + line.len += 1; + } + line.append(&swap_layout_status); + } + line +} + +fn generate_default_keys(help: &ModeInfo) -> Vec { let binds = &help.get_mode_keybinds(); - // Unselect all by default - let mut default_keys = vec![ + vec![ KeyShortcut::new( KeyMode::Unselected, KeyAction::Lock, @@ -512,7 +809,20 @@ pub fn first_line( KeyAction::Quit, to_char(action_key(binds, &[Action::Quit])), ), - ]; + ] +} + +pub fn first_line( + help: &ModeInfo, + tab_info: Option<&TabInfo>, + max_len: usize, + separator: &str, +) -> LinePart { + let supports_arrow_fonts = !help.capabilities.arrow_fonts; + let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts); + let binds = &help.get_mode_keybinds(); + // Unselect all by default + let mut default_keys = generate_default_keys(help); // TODO: check that this still works if let Some(key_shortcut) = get_key_shortcut_for_mode(&mut default_keys, &help.mode) { key_shortcut.mode = KeyMode::Selected; @@ -539,25 +849,14 @@ pub fn first_line( key_indicators(max_len, &default_keys, colored_elements, separator, help); if key_indicators.len < max_len { if let Some(tab_info) = tab_info { - let mut remaining_space = max_len - key_indicators.len; - if let Some(swap_layout_status) = swap_layout_status( + let remaining_space = max_len - key_indicators.len; + key_indicators.append(&swap_layout_status_and_padding( + &tab_info, remaining_space, - &tab_info.active_swap_layout_name, - tab_info.is_swap_layout_dirty, - help, - colored_elements, - &help.style.colors, separator, - ) { - remaining_space -= swap_layout_status.len; - for _ in 0..remaining_space { - key_indicators.part.push_str( - &ANSIStrings(&[colored_elements.superkey_prefix.paint(" ")]).to_string(), - ); - key_indicators.len += 1; - } - key_indicators.append(&swap_layout_status); - } + colored_elements, + help, + )); } } key_indicators diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs index 3e66855963..ee14e45d54 100644 --- a/default-plugins/status-bar/src/main.rs +++ b/default-plugins/status-bar/src/main.rs @@ -13,10 +13,9 @@ use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::*; use zellij_tile_utils::{palette_match, style}; -use first_line::first_line; +use first_line::{first_line, first_line_supermode}; use second_line::{ - floating_panes_are_visible, fullscreen_panes_to_hide, keybinds, - locked_floating_panes_are_visible, locked_fullscreen_panes_to_hide, system_clipboard_error, + floating_panes_are_visible, fullscreen_panes_to_hide, keybinds, system_clipboard_error, text_copied_hint, }; use tip::utils::get_cached_tip_name; @@ -34,6 +33,10 @@ struct State { mode_info: ModeInfo, text_copy_destination: Option, display_system_clipboard_failure: bool, + + supermode: bool, + standby_mode: InputMode, + current_mode: InputMode, } register_plugin!(State); @@ -60,6 +63,7 @@ impl Display for LinePart { #[derive(Clone, Copy)] pub struct ColoredElements { pub selected: SegmentStyle, + pub selected_standby_shortcut: SegmentStyle, pub unselected: SegmentStyle, pub unselected_alternate: SegmentStyle, pub disabled: SegmentStyle, @@ -109,6 +113,14 @@ fn color_elements(palette: Palette, different_color_alternates: bool) -> Colored styled_text: style!(background, palette.green).bold(), suffix_separator: style!(palette.green, background).bold(), }, + selected_standby_shortcut: SegmentStyle { + prefix_separator: style!(background, palette.green), + char_left_separator: style!(background, palette.green).bold(), + char_shortcut: style!(palette.red, palette.green).bold(), + char_right_separator: style!(background, palette.green).bold(), + styled_text: style!(background, palette.green).bold(), + suffix_separator: style!(palette.green, palette.fg).bold(), + }, unselected: SegmentStyle { prefix_separator: style!(background, palette.fg), char_left_separator: style!(background, palette.fg).bold(), @@ -145,6 +157,14 @@ fn color_elements(palette: Palette, different_color_alternates: bool) -> Colored styled_text: style!(background, palette.green).bold(), suffix_separator: style!(palette.green, background).bold(), }, + selected_standby_shortcut: SegmentStyle { + prefix_separator: style!(background, palette.green), + char_left_separator: style!(background, palette.green).bold(), + char_shortcut: style!(palette.red, palette.green).bold(), + char_right_separator: style!(background, palette.green).bold(), + styled_text: style!(background, palette.green).bold(), + suffix_separator: style!(palette.green, palette.fg).bold(), + }, unselected: SegmentStyle { prefix_separator: style!(background, palette.fg), char_left_separator: style!(background, palette.fg).bold(), @@ -187,12 +207,44 @@ impl ZellijPlugin for State { EventType::InputReceived, EventType::SystemClipboardFailure, ]); + self.supermode = false; // TODO: from config + self.standby_mode = InputMode::Pane; + if self.supermode { + switch_to_input_mode(&InputMode::Locked); // supermode should start locked (TODO: only + // once per app load, let's not do this on + // each tab loading) + } } fn update(&mut self, event: Event) -> bool { let mut should_render = false; match event { Event::ModeUpdate(mode_info) => { + if self.supermode { + // supermode is a "sticky" mode that is not Normal or Locked + // using this configuration, we default to Locked mode in order to avoid key + // collisions with terminal applications + // whenever the user switches away from locked mode, we make sure to place them + // in the standby mode + // whenever the user switches to a mode that is not locked or normal, we set + // that mode as the standby mode + // whenever the user switches away from the standby mode, we palce them in + // normal mode + if mode_info.mode == InputMode::Normal && self.current_mode == InputMode::Locked + { + switch_to_input_mode(&self.standby_mode); + } else if mode_info.mode == InputMode::Normal + && self.current_mode == self.standby_mode + { + switch_to_input_mode(&InputMode::Locked); + } else if mode_info.mode != InputMode::Locked + && mode_info.mode != InputMode::Normal + { + self.standby_mode = mode_info.mode; + } + self.current_mode = mode_info.mode; + } + if self.mode_info != mode_info { should_render = true; } @@ -244,7 +296,17 @@ impl ZellijPlugin for State { }; let active_tab = self.tabs.iter().find(|t| t.active); - let first_line = first_line(&self.mode_info, active_tab, cols, separator); + let first_line = if self.supermode { + first_line_supermode( + &self.standby_mode, + &self.mode_info, + active_tab, + cols, + separator, + ) + } else { + first_line(&self.mode_info, active_tab, cols, separator) + }; let second_line = self.second_line(cols); let background = match self.mode_info.style.colors.theme_hue { @@ -296,11 +358,7 @@ impl State { } else if let Some(active_tab) = active_tab { if active_tab.is_fullscreen_active { match self.mode_info.mode { - InputMode::Normal => fullscreen_panes_to_hide( - &self.mode_info.style.colors, - active_tab.panes_to_hide, - ), - InputMode::Locked => locked_fullscreen_panes_to_hide( + InputMode::Normal | InputMode::Locked => fullscreen_panes_to_hide( &self.mode_info.style.colors, active_tab.panes_to_hide, ), @@ -308,9 +366,8 @@ impl State { } } else if active_tab.are_floating_panes_visible { match self.mode_info.mode { - InputMode::Normal => floating_panes_are_visible(&self.mode_info), - InputMode::Locked => { - locked_floating_panes_are_visible(&self.mode_info.style.colors) + InputMode::Normal | InputMode::Locked => { + floating_panes_are_visible(&self.mode_info) }, _ => keybinds(&self.mode_info, &self.tip_name, cols), } diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs index b7c8a9cecc..015618f06f 100644 --- a/default-plugins/status-bar/src/second_line.rs +++ b/default-plugins/status-bar/src/second_line.rs @@ -45,20 +45,6 @@ fn full_length_shortcut( } } -fn locked_interface_indication(palette: Palette) -> LinePart { - let locked_text = " -- INTERFACE LOCKED -- "; - let locked_text_len = locked_text.chars().count(); - let text_color = palette_match!(match palette.theme_hue { - ThemeHue::Dark => palette.white, - ThemeHue::Light => palette.black, - }); - let locked_styled_text = Style::new().fg(text_color).bold().paint(locked_text); - LinePart { - part: locked_styled_text.to_string(), - len: locked_text_len, - } -} - fn add_shortcut(help: &ModeInfo, linepart: &LinePart, text: &str, keys: Vec) -> LinePart { let shortcut = if linepart.len == 0 { full_length_shortcut(true, keys, text, help.style.colors) @@ -144,21 +130,17 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec)> { } if mi.mode == IM::Pane { vec![ - (s("Move focus"), s("Move"), + (s("New"), s("New"), action_key(&km, &[A::NewPane(None, None), TO_NORMAL])), + (s("Change Focus"), s("Move"), action_key_group(&km, &[&[A::MoveFocus(Dir::Left)], &[A::MoveFocus(Dir::Down)], &[A::MoveFocus(Dir::Up)], &[A::MoveFocus(Dir::Right)]])), - (s("New"), s("New"), action_key(&km, &[A::NewPane(None, None), TO_NORMAL])), (s("Close"), s("Close"), action_key(&km, &[A::CloseFocus, TO_NORMAL])), (s("Rename"), s("Rename"), action_key(&km, &[A::SwitchToMode(IM::RenamePane), A::PaneNameInput(vec![0])])), - (s("Split down"), s("Down"), action_key(&km, &[A::NewPane(Some(Dir::Down), None), TO_NORMAL])), - (s("Split right"), s("Right"), action_key(&km, &[A::NewPane(Some(Dir::Right), None), TO_NORMAL])), - (s("Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])), - (s("Frames"), s("Frames"), action_key(&km, &[A::TogglePaneFrames, TO_NORMAL])), - (s("Floating toggle"), s("Floating"), + (s("Toggle Fullscreen"), s("Fullscreen"), action_key(&km, &[A::ToggleFocusFullscreen, TO_NORMAL])), + (s("Toggle Floating"), s("Floating"), action_key(&km, &[A::ToggleFloatingPanes, TO_NORMAL])), - (s("Embed pane"), s("Embed"), action_key(&km, &[A::TogglePaneEmbedOrFloating, TO_NORMAL])), - (s("Next"), s("Next"), action_key(&km, &[A::SwitchFocus])), + (s("Toggle Embed"), s("Embed"), action_key(&km, &[A::TogglePaneEmbedOrFloating, TO_NORMAL])), (s("Select pane"), s("Select"), to_normal_key), ]} else if mi.mode == IM::Tab { // With the default bindings, "Move focus" for tabs is tricky: It binds all the arrow keys @@ -178,8 +160,8 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec)> { }; vec![ - (s("Move focus"), s("Move"), focus_keys), (s("New"), s("New"), action_key(&km, &[A::NewTab(None, vec![], None, None, None), TO_NORMAL])), + (s("Change focus"), s("Move"), focus_keys), (s("Close"), s("Close"), action_key(&km, &[A::CloseTab, TO_NORMAL])), (s("Rename"), s("Rename"), action_key(&km, &[A::SwitchToMode(IM::RenameTab), A::TabNameInput(vec![0])])), @@ -187,6 +169,11 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec)> { (s("Toggle"), s("Toggle"), action_key(&km, &[A::ToggleTab])), (s("Select pane"), s("Select"), to_normal_key), ]} else if mi.mode == IM::Resize { vec![ + (s("Increase/Decrease size"), s("Increase/Decrease"), + action_key_group(&km, &[ + &[A::Resize(Resize::Increase, None)], + &[A::Resize(Resize::Decrease, None)] + ])), (s("Increase to"), s("Increase"), action_key_group(&km, &[ &[A::Resize(Resize::Increase, Some(Dir::Left))], &[A::Resize(Resize::Increase, Some(Dir::Down))], @@ -199,19 +186,14 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec)> { &[A::Resize(Resize::Decrease, Some(Dir::Up))], &[A::Resize(Resize::Decrease, Some(Dir::Right))] ])), - (s("Increase/Decrease size"), s("Increase/Decrease"), - action_key_group(&km, &[ - &[A::Resize(Resize::Increase, None)], - &[A::Resize(Resize::Decrease, None)] - ])), (s("Select pane"), s("Select"), to_normal_key), ]} else if mi.mode == IM::Move { vec![ - (s("Move"), s("Move"), action_key_group(&km, &[ + (s("Switch Location"), s("Move"), action_key_group(&km, &[ &[Action::MovePane(Some(Dir::Left))], &[Action::MovePane(Some(Dir::Down))], &[Action::MovePane(Some(Dir::Up))], &[Action::MovePane(Some(Dir::Right))]])), - (s("Next pane"), s("Next"), action_key(&km, &[Action::MovePane(None)])), - (s("Previous pane"), s("Previous"), action_key(&km, &[Action::MovePaneBackwards])), ]} else if mi.mode == IM::Scroll { vec![ + (s("Enter search term"), s("Search"), + action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])), (s("Scroll"), s("Scroll"), action_key_group(&km, &[&[Action::ScrollDown], &[Action::ScrollUp]])), (s("Scroll page"), s("Scroll"), @@ -220,22 +202,20 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec)> { action_key_group(&km, &[&[Action::HalfPageScrollDown], &[Action::HalfPageScrollUp]])), (s("Edit scrollback in default editor"), s("Edit"), action_key(&km, &[Action::EditScrollback, TO_NORMAL])), - (s("Enter search term"), s("Search"), - action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])), (s("Select pane"), s("Select"), to_normal_key), ]} else if mi.mode == IM::EnterSearch { vec![ (s("When done"), s("Done"), action_key(&km, &[A::SwitchToMode(IM::Search)])), (s("Cancel"), s("Cancel"), action_key(&km, &[A::SearchInput(vec![27]), A::SwitchToMode(IM::Scroll)])), ]} else if mi.mode == IM::Search { vec![ + (s("Enter Search term"), s("Search"), + action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])), (s("Scroll"), s("Scroll"), action_key_group(&km, &[&[Action::ScrollDown], &[Action::ScrollUp]])), (s("Scroll page"), s("Scroll"), action_key_group(&km, &[&[Action::PageScrollDown], &[Action::PageScrollUp]])), (s("Scroll half page"), s("Scroll"), action_key_group(&km, &[&[Action::HalfPageScrollDown], &[Action::HalfPageScrollUp]])), - (s("Enter term"), s("Search"), - action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])), (s("Search down"), s("Down"), action_key(&km, &[A::Search(SDir::Down)])), (s("Search up"), s("Up"), action_key(&km, &[A::Search(SDir::Up)])), (s("Case sensitive"), s("Case"), @@ -270,8 +250,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec)> { fn full_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart { match help.mode { - InputMode::Normal => tip(help), - InputMode::Locked => locked_interface_indication(help.style.colors), + InputMode::Normal | InputMode::Locked => tip(help), _ => full_shortcut_list_nonstandard_mode(help), } } @@ -288,8 +267,7 @@ fn shortened_shortcut_list_nonstandard_mode(help: &ModeInfo) -> LinePart { fn shortened_shortcut_list(help: &ModeInfo, tip: TipFn) -> LinePart { match help.mode { - InputMode::Normal => tip(help), - InputMode::Locked => locked_interface_indication(help.style.colors), + InputMode::Normal | InputMode::Locked => tip(help), _ => shortened_shortcut_list_nonstandard_mode(help), } } @@ -312,7 +290,7 @@ fn best_effort_shortcut_list_nonstandard_mode(help: &ModeInfo, max_len: usize) - fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> LinePart { match help.mode { - InputMode::Normal => { + InputMode::Normal | InputMode::Locked => { let line_part = tip(help); if line_part.len <= max_len { line_part @@ -320,14 +298,6 @@ fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> Lin LinePart::default() } }, - InputMode::Locked => { - let line_part = locked_interface_indication(help.style.colors); - if line_part.len <= max_len { - line_part - } else { - LinePart::default() - } - }, _ => best_effort_shortcut_list_nonstandard_mode(help, max_len), } } @@ -472,68 +442,6 @@ pub fn floating_panes_are_visible(mode_info: &ModeInfo) -> LinePart { } } -pub fn locked_fullscreen_panes_to_hide(palette: &Palette, panes_to_hide: usize) -> LinePart { - let text_color = palette_match!(match palette.theme_hue { - ThemeHue::Dark => palette.white, - ThemeHue::Light => palette.black, - }); - let green_color = palette_match!(palette.green); - let orange_color = palette_match!(palette.orange); - let locked_text = " -- INTERFACE LOCKED -- "; - let shortcut_left_separator = Style::new().fg(text_color).bold().paint(" ("); - let shortcut_right_separator = Style::new().fg(text_color).bold().paint("): "); - let fullscreen = "FULLSCREEN"; - let puls = "+ "; - let panes = panes_to_hide.to_string(); - let hide = " hidden panes"; - let len = locked_text.chars().count() - + fullscreen.chars().count() - + puls.chars().count() - + panes.chars().count() - + hide.chars().count() - + 5; // 3 for ():'s around shortcut, 2 for the space - LinePart { - part: format!( - "{}{}{}{}{}{}{}", - Style::new().fg(text_color).bold().paint(locked_text), - shortcut_left_separator, - Style::new().fg(orange_color).bold().paint(fullscreen), - shortcut_right_separator, - Style::new().fg(text_color).bold().paint(puls), - Style::new().fg(green_color).bold().paint(panes), - Style::new().fg(text_color).bold().paint(hide) - ), - len, - } -} - -pub fn locked_floating_panes_are_visible(palette: &Palette) -> LinePart { - let white_color = match palette.white { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - let orange_color = match palette.orange { - PaletteColor::Rgb((r, g, b)) => RGB(r, g, b), - PaletteColor::EightBit(color) => Fixed(color), - }; - let shortcut_left_separator = Style::new().fg(white_color).bold().paint(" ("); - let shortcut_right_separator = Style::new().fg(white_color).bold().paint(")"); - let locked_text = " -- INTERFACE LOCKED -- "; - let floating_panes = "FLOATING PANES VISIBLE"; - - let len = locked_text.chars().count() + floating_panes.chars().count(); - LinePart { - part: format!( - "{}{}{}{}", - Style::new().fg(white_color).bold().paint(locked_text), - shortcut_left_separator, - Style::new().fg(orange_color).bold().paint(floating_panes), - shortcut_right_separator, - ), - len, - } -} - #[cfg(test)] /// Unit tests. /// @@ -668,7 +576,6 @@ mod tests { assert_eq!(ret, " / Ctrl + Foobar"); } - //pub fn keybinds(help: &ModeInfo, tip_name: &str, max_width: usize) -> LinePart { #[test] // Note how it leaves out elements that don't exist! @@ -698,7 +605,7 @@ mod tests { assert_eq!( ret, - " <←↓↑→> Move focus / New / Close / Fullscreen" + " New / <←↓↑→> Change Focus / Close / Toggle Fullscreen", ); } @@ -728,7 +635,7 @@ mod tests { let ret = keybinds(&mode_info, "quicknav", 35); let ret = unstyle(ret); - assert_eq!(ret, " <←↓↑→> Move / New ... "); + assert_eq!(ret, " New / <←↓↑→> Move ... "); } #[test] @@ -753,6 +660,9 @@ mod tests { let ret = keybinds(&mode_info, "quicknav", 500); let ret = unstyle(ret); - assert_eq!(ret, " Ctrl + Move focus / New / Close / Fullscreen"); + assert_eq!( + ret, + " New / Ctrl + Change Focus / Close / Toggle Fullscreen" + ); } } diff --git a/default-plugins/status-bar/src/tip/data/compact_layout.rs b/default-plugins/status-bar/src/tip/data/compact_layout.rs index 77dec8a60f..13ee18bd3b 100644 --- a/default-plugins/status-bar/src/tip/data/compact_layout.rs +++ b/default-plugins/status-bar/src/tip/data/compact_layout.rs @@ -77,7 +77,7 @@ pub fn compact_layout_short(help: &ModeInfo) -> LinePart { fn add_keybinds(help: &ModeInfo) -> Vec { let to_pane = action_key( - &help.get_mode_keybinds(), + &help.get_keybinds_for_mode(InputMode::Normal), &[Action::SwitchToMode(InputMode::Pane)], ); let pane_frames = action_key( diff --git a/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs b/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs index ebd944b779..9ff210edec 100644 --- a/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs +++ b/default-plugins/status-bar/src/tip/data/edit_scrollbuffer.rs @@ -67,7 +67,7 @@ pub fn edit_scrollbuffer_short(help: &ModeInfo) -> LinePart { fn add_keybinds(help: &ModeInfo) -> Vec { let to_pane = action_key( - &help.get_mode_keybinds(), + &help.get_keybinds_for_mode(InputMode::Normal), &[Action::SwitchToMode(InputMode::Scroll)], ); let edit_buffer = action_key( diff --git a/default-plugins/status-bar/src/tip/data/floating_panes_mouse.rs b/default-plugins/status-bar/src/tip/data/floating_panes_mouse.rs index 4bf4a143dc..17439c4bf7 100644 --- a/default-plugins/status-bar/src/tip/data/floating_panes_mouse.rs +++ b/default-plugins/status-bar/src/tip/data/floating_panes_mouse.rs @@ -46,7 +46,7 @@ pub fn floating_panes_mouse_short(help: &ModeInfo) -> LinePart { fn add_keybinds(help: &ModeInfo) -> Vec { let to_pane = action_key( - &help.get_mode_keybinds(), + &help.get_keybinds_for_mode(InputMode::Normal), &[Action::SwitchToMode(InputMode::Pane)], ); let floating_toggle = action_key( diff --git a/default-plugins/status-bar/src/tip/data/quicknav.rs b/default-plugins/status-bar/src/tip/data/quicknav.rs index 318fe70258..fe118f2c49 100644 --- a/default-plugins/status-bar/src/tip/data/quicknav.rs +++ b/default-plugins/status-bar/src/tip/data/quicknav.rs @@ -61,7 +61,7 @@ struct Keygroups<'a> { } fn add_keybinds(help: &ModeInfo) -> Keygroups { - let normal_keymap = help.get_mode_keybinds(); + let normal_keymap = help.get_keybinds_for_mode(InputMode::Normal); let new_pane_keys = action_key(&normal_keymap, &[Action::NewPane(None, None)]); let new_pane = if new_pane_keys.is_empty() { vec![Style::new().bold().paint("UNBOUND")] diff --git a/default-plugins/status-bar/src/tip/data/sync_tab.rs b/default-plugins/status-bar/src/tip/data/sync_tab.rs index 486e2aa242..71a4ed50ae 100644 --- a/default-plugins/status-bar/src/tip/data/sync_tab.rs +++ b/default-plugins/status-bar/src/tip/data/sync_tab.rs @@ -45,7 +45,7 @@ pub fn sync_tab_short(help: &ModeInfo) -> LinePart { fn add_keybinds(help: &ModeInfo) -> Vec { let to_tab = action_key( - &help.get_mode_keybinds(), + &help.get_keybinds_for_mode(InputMode::Normal), &[Action::SwitchToMode(InputMode::Tab)], ); let sync_tabs = action_key( diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index b34138b959..b411b4ea93 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -808,7 +808,7 @@ pub fn lock_mode() { name: "Send keys that should not be intercepted by the app", instruction: |mut remote_terminal: RemoteTerminal| -> bool { let mut step_is_complete = false; - if remote_terminal.snapshot_contains("INTERFACE LOCKED") { + if remote_terminal.snapshot_contains("<> PANE") { remote_terminal.send_key(&TAB_MODE); remote_terminal.send_key(&NEW_TAB_IN_TAB_MODE); remote_terminal.send_key("abc".as_bytes()); diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap index e6d38062e9..209b67791e 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap @@ -1,6 +1,6 @@ --- source: src/tests/e2e/cases.rs -assertion_line: 836 +assertion_line: 840 expression: last_snapshot --- Zellij (e2e-test)  Tab #1  @@ -26,4 +26,4 @@ expression: last_snapshot │ │ └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ Ctrl + LOCK  <> PANE  <> TAB  <> RESIZE  <> MOVE  <> SEARCH  <> SESSION  <> QUIT  - -- INTERFACE LOCKED -- + Tip: Alt + => new pane. Alt + <←↓↑→> or Alt + => navigate. Alt + <+|-> => resize pane. diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap index 34256a877d..205ff40c73 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap @@ -1,6 +1,6 @@ --- source: src/tests/e2e/cases.rs -assertion_line: 1341 +assertion_line: 1354 expression: second_runner_snapshot --- Zellij (mirrored_sessions)  Tab #1  Tab #2  @@ -26,4 +26,4 @@ expression: second_runner_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - <←→> Move focus / New / Close / Rename / Sync / Toggle / Select pane + New / <←→> Change focus / Close / Rename / Sync / Toggle / Select pane diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap index 524969c40c..e1d36e7cc3 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__scrolling_inside_a_pane.snap @@ -1,6 +1,6 @@ --- source: src/tests/e2e/cases.rs -assertion_line: 290 +assertion_line: 305 expression: last_snapshot --- Zellij (e2e-test)  Tab #1  @@ -26,4 +26,4 @@ expression: last_snapshot │ ││li█e21 │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - <↓↑> Scroll / Scroll / Scroll / Edit / Search / Select + Search / <↓↑> Scroll / Scroll / Scroll / Edit / Select From a09b86e41d0be2f61e49eb550ed69db306fd2a65 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 12 Jul 2023 11:32:56 +0200 Subject: [PATCH 011/128] docs(changelog): status-bar supermode --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2e20c83b..452e938df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] * fix(tab-bar,compact-bar): tab switching with mouse sometimes not working (https://github.com/zellij-org/zellij/pull/2587) +* feat(status-bar): supermode to prevent colliding keybindings ## [0.37.2] - 2023-06-20 * hotfix: include theme files into binary (https://github.com/zellij-org/zellij/pull/2566) From 7e60a769040646f0af512fba369b6cdd6551f4c6 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 12 Jul 2023 20:30:41 +0200 Subject: [PATCH 012/128] fix(rendering): occasional glitches while resizing (#2621) --- zellij-server/src/os_input_output.rs | 29 +++++++++ zellij-server/src/pty_writer.rs | 44 +++++++++++++- zellij-server/src/screen.rs | 5 ++ zellij-server/src/tab/mod.rs | 59 +++++++++++-------- .../src/tab/unit/tab_integration_tests.rs | 1 + ...end_cli_toggle_active_tab_sync_action.snap | 4 +- ...ests__send_cli_write_action_to_screen.snap | 4 +- ...send_cli_write_chars_action_to_screen.snap | 4 +- zellij-utils/src/errors.rs | 3 + 9 files changed, 120 insertions(+), 33 deletions(-) diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index 18e8f92db9..f4cbf9b4a3 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -10,6 +10,7 @@ use nix::{ }, unistd, }; + use signal_hook::consts::*; use sysinfo::{ProcessExt, ProcessRefreshKind, System, SystemExt}; use zellij_utils::{ @@ -840,6 +841,34 @@ pub fn get_server_os_input() -> Result { }) } +use crate::pty_writer::PtyWriteInstruction; +use crate::thread_bus::ThreadSenders; + +pub struct ResizeCache { + senders: ThreadSenders, +} + +impl ResizeCache { + pub fn new(senders: ThreadSenders) -> Self { + senders + .send_to_pty_writer(PtyWriteInstruction::StartCachingResizes) + .unwrap_or_else(|e| { + log::error!("Failed to cache resizes: {}", e); + }); + ResizeCache { senders } + } +} + +impl Drop for ResizeCache { + fn drop(&mut self) { + self.senders + .send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes) + .unwrap_or_else(|e| { + log::error!("Failed to apply cached resizes: {}", e); + }); + } +} + /// Process id's for forked terminals #[derive(Debug)] pub struct ChildId { diff --git a/zellij-server/src/pty_writer.rs b/zellij-server/src/pty_writer.rs index 9c3b6d49ae..017fed39af 100644 --- a/zellij-server/src/pty_writer.rs +++ b/zellij-server/src/pty_writer.rs @@ -2,9 +2,16 @@ use zellij_utils::errors::{prelude::*, ContextType, PtyWriteContext}; use crate::thread_bus::Bus; +// we separate these instruction to a different thread because some programs get deadlocked if +// you write into their STDIN while reading from their STDOUT (I'm looking at you, vim) +// while the same has not been observed to happen with resizes, it could conceivably happen and we have this +// here anyway, so #[derive(Debug, Clone, Eq, PartialEq)] pub enum PtyWriteInstruction { Write(Vec, u32), + ResizePty(u32, u16, u16, Option, Option), // terminal_id, columns, rows, pixel width, pixel height + StartCachingResizes, + ApplyCachedResizes, Exit, } @@ -12,6 +19,9 @@ impl From<&PtyWriteInstruction> for PtyWriteContext { fn from(tty_write_instruction: &PtyWriteInstruction) -> Self { match *tty_write_instruction { PtyWriteInstruction::Write(..) => PtyWriteContext::Write, + PtyWriteInstruction::ResizePty(..) => PtyWriteContext::ResizePty, + PtyWriteInstruction::ApplyCachedResizes => PtyWriteContext::ApplyCachedResizes, + PtyWriteInstruction::StartCachingResizes => PtyWriteContext::StartCachingResizes, PtyWriteInstruction::Exit => PtyWriteContext::Exit, } } @@ -23,7 +33,7 @@ pub(crate) fn pty_writer_main(bus: Bus) -> Result<()> { loop { let (event, mut err_ctx) = bus.recv().with_context(err_context)?; err_ctx.add_call(ContextType::PtyWrite((&event).into())); - let os_input = bus + let mut os_input = bus .os_input .clone() .context("no OS input API found") @@ -39,6 +49,38 @@ pub(crate) fn pty_writer_main(bus: Bus) -> Result<()> { .with_context(err_context) .non_fatal(); }, + PtyWriteInstruction::ResizePty( + terminal_id, + columns, + rows, + width_in_pixels, + height_in_pixels, + ) => { + os_input + .set_terminal_size_using_terminal_id( + terminal_id, + columns, + rows, + width_in_pixels, + height_in_pixels, + ) + .with_context(err_context) + .non_fatal(); + }, + PtyWriteInstruction::StartCachingResizes => { + // we do this because there are some logic traps inside the screen/tab/layout code + // the cause multiple resizes to be sent to the pty - while the last one is always + // the correct one, many programs and shells debounce those (I guess due to the + // trauma of dealing with GUI resizes of the controlling terminal window), and this + // then causes glitches and missing redraws + // so we do this to play nice and always only send the last resize instruction to + // each pane + // the logic for this happens in the main Screen event loop + os_input.cache_resizes(); + }, + PtyWriteInstruction::ApplyCachedResizes => { + os_input.apply_cached_resizes(); + }, PtyWriteInstruction::Exit => { return Ok(()); }, diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 97802cb451..fc86b4faef 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -21,6 +21,7 @@ use zellij_utils::{ position::Position, }; +use crate::os_input_output::ResizeCache; use crate::panes::alacritty_functions::xparse_color; use crate::panes::terminal_character::AnsiCode; @@ -1586,6 +1587,7 @@ pub(crate) fn screen_thread_main( config_options.copy_on_select.unwrap_or(true), ); + let thread_senders = bus.senders.clone(); let mut screen = Screen::new( bus, &client_attributes, @@ -1614,6 +1616,9 @@ pub(crate) fn screen_thread_main( .recv() .context("failed to receive event on channel")?; err_ctx.add_call(ContextType::Screen((&event).into())); + // here we start caching resizes, so that we'll send them in bulk at the end of each event + // when this cache is Dropped, for more information, see the comments in PtyWriter + let _resize_cache = ResizeCache::new(thread_senders.clone()); match event { ScreenInstruction::PtyBytes(pid, vte_bytes) => { diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 62b4c40579..27feb5034b 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -59,13 +59,17 @@ use zellij_utils::{ macro_rules! resize_pty { ($pane:expr, $os_input:expr, $senders:expr) => {{ match $pane.pid() { - PaneId::Terminal(ref pid) => $os_input.set_terminal_size_using_terminal_id( - *pid, - $pane.get_content_columns() as u16, - $pane.get_content_rows() as u16, - None, - None, - ), + PaneId::Terminal(ref pid) => { + $senders + .send_to_pty_writer(PtyWriteInstruction::ResizePty( + *pid, + $pane.get_content_columns() as u16, + $pane.get_content_rows() as u16, + None, + None, + )) + .with_context(err_context); + }, PaneId::Plugin(ref pid) => { let err_context = || format!("failed to resize plugin {pid}"); $senders @@ -93,13 +97,19 @@ macro_rules! resize_pty { } }; match $pane.pid() { - PaneId::Terminal(ref pid) => $os_input.set_terminal_size_using_terminal_id( - *pid, - $pane.get_content_columns() as u16, - $pane.get_content_rows() as u16, - width_in_pixels, - height_in_pixels, - ), + PaneId::Terminal(ref pid) => { + use crate::PtyWriteInstruction; + let err_context = || format!("Failed to send resize pty instruction"); + $senders + .send_to_pty_writer(PtyWriteInstruction::ResizePty( + *pid, + $pane.get_content_columns() as u16, + $pane.get_content_rows() as u16, + width_in_pixels, + height_in_pixels, + )) + .with_context(err_context) + }, PaneId::Plugin(ref pid) => { let err_context = || format!("failed to resize plugin {pid}"); $senders @@ -758,16 +768,15 @@ impl Tab { Ok(()) } pub fn previous_swap_layout(&mut self, client_id: Option) -> Result<()> { - // warning, here we cache resizes rather than sending them to the pty, we do that in - // apply_cached_resizes below - beware when bailing on this function early! - self.os_api.cache_resizes(); let search_backwards = true; if self.floating_panes.panes_are_visible() { self.relayout_floating_panes(client_id, search_backwards, true)?; } else { self.relayout_tiled_panes(client_id, search_backwards, true, false)?; } - self.os_api.apply_cached_resizes(); + self.senders + .send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes) + .with_context(|| format!("failed to update plugins with mode info"))?; Ok(()) } pub fn next_swap_layout( @@ -775,16 +784,15 @@ impl Tab { client_id: Option, refocus_pane: bool, ) -> Result<()> { - // warning, here we cache resizes rather than sending them to the pty, we do that in - // apply_cached_resizes below - beware when bailing on this function early! - self.os_api.cache_resizes(); let search_backwards = false; if self.floating_panes.panes_are_visible() { self.relayout_floating_panes(client_id, search_backwards, refocus_pane)?; } else { self.relayout_tiled_panes(client_id, search_backwards, refocus_pane, false)?; } - self.os_api.apply_cached_resizes(); + self.senders + .send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes) + .with_context(|| format!("failed to update plugins with mode info"))?; Ok(()) } pub fn apply_buffered_instructions(&mut self) -> Result<()> { @@ -1807,9 +1815,6 @@ impl Tab { selectable_tiled_panes.count() > 0 } pub fn resize_whole_tab(&mut self, new_screen_size: Size) -> Result<()> { - // warning, here we cache resizes rather than sending them to the pty, we do that in - // apply_cached_resizes below - beware when bailing on this function early! - self.os_api.cache_resizes(); let err_context = || format!("failed to resize whole tab (index {})", self.index); self.floating_panes.resize(new_screen_size); // we need to do this explicitly because floating_panes.resize does not do this @@ -1829,7 +1834,9 @@ impl Tab { let _ = self.relayout_tiled_panes(None, false, false, true); } self.should_clear_display_before_rendering = true; - let _ = self.os_api.apply_cached_resizes(); + self.senders + .send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes) + .with_context(|| format!("failed to update plugins with mode info"))?; Ok(()) } pub fn resize(&mut self, client_id: ClientId, strategy: ResizeStrategy) -> Result<()> { diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index c870c3a0b7..5a04d9c145 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -175,6 +175,7 @@ impl MockPtyInstructionBus { .unwrap() .push(String::from_utf8_lossy(&msg).to_string()), PtyWriteInstruction::Exit => break, + _ => {}, } } }) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_toggle_active_tab_sync_action.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_toggle_active_tab_sync_action.snap index 8597c63091..1c2440eb85 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_toggle_active_tab_sync_action.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_toggle_active_tab_sync_action.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 1487 +assertion_line: 1825 expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" --- -[Write([102, 111, 111], 0), Write([102, 111, 111], 1), Exit] +[StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([102, 111, 111], 0), Write([102, 111, 111], 1), ApplyCachedResizes, Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_write_action_to_screen.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_write_action_to_screen.snap index 32ae2a500d..aad2ad25c5 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_write_action_to_screen.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_write_action_to_screen.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 879 +assertion_line: 1065 expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" --- -[Write([102, 111, 111], 0), Exit] +[StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([102, 111, 111], 0), ApplyCachedResizes, Exit] diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_write_chars_action_to_screen.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_write_chars_action_to_screen.snap index 90ade0ee52..ca6c9635e5 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_write_chars_action_to_screen.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_write_chars_action_to_screen.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 846 +assertion_line: 1039 expression: "format!(\"{:?}\", * received_pty_instructions.lock().unwrap())" --- -[Write([105, 110, 112, 117, 116, 32, 102, 114, 111, 109, 32, 116, 104, 101, 32, 99, 108, 105], 0), Exit] +[StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([105, 110, 112, 117, 116, 32, 102, 114, 111, 109, 32, 116, 104, 101, 32, 99, 108, 105], 0), ApplyCachedResizes, Exit] diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index b8f997a801..f342d6780d 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -414,6 +414,9 @@ pub enum ServerContext { #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum PtyWriteContext { Write, + ResizePty, + StartCachingResizes, + ApplyCachedResizes, Exit, } From e4ab2ec578873d2137313412b41438cdf1af0019 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 12 Jul 2023 20:37:03 +0200 Subject: [PATCH 013/128] docs(changelog): resize glitches fix --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 452e938df7..71d30f44e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] * fix(tab-bar,compact-bar): tab switching with mouse sometimes not working (https://github.com/zellij-org/zellij/pull/2587) -* feat(status-bar): supermode to prevent colliding keybindings +* feat(status-bar): supermode to prevent colliding keybindings (https://github.com/zellij-org/zellij/pull/2619) +* fix(rendering): occasional glitches while resizing (https://github.com/zellij-org/zellij/pull/2621) ## [0.37.2] - 2023-06-20 * hotfix: include theme files into binary (https://github.com/zellij-org/zellij/pull/2566) From d71e9946c65e7efba846b80f2cb2815531792844 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 12 Jul 2023 20:32:53 +0200 Subject: [PATCH 014/128] chore(version): bump development version --- Cargo.lock | 12 ++++++------ Cargo.toml | 8 ++++---- zellij-client/Cargo.toml | 4 ++-- zellij-server/Cargo.toml | 4 ++-- zellij-tile-utils/Cargo.toml | 2 +- zellij-tile/Cargo.toml | 4 ++-- zellij-utils/Cargo.toml | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3e1d0be07..83d566361a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4427,7 +4427,7 @@ dependencies = [ [[package]] name = "zellij" -version = "0.37.2" +version = "0.38.0" dependencies = [ "dialoguer", "insta", @@ -4445,7 +4445,7 @@ dependencies = [ [[package]] name = "zellij-client" -version = "0.37.2" +version = "0.38.0" dependencies = [ "insta", "log", @@ -4459,7 +4459,7 @@ dependencies = [ [[package]] name = "zellij-server" -version = "0.37.2" +version = "0.38.0" dependencies = [ "ansi_term", "arrayvec 0.7.2", @@ -4490,7 +4490,7 @@ dependencies = [ [[package]] name = "zellij-tile" -version = "0.37.2" +version = "0.38.0" dependencies = [ "clap", "serde", @@ -4502,14 +4502,14 @@ dependencies = [ [[package]] name = "zellij-tile-utils" -version = "0.37.2" +version = "0.38.0" dependencies = [ "ansi_term", ] [[package]] name = "zellij-utils" -version = "0.37.2" +version = "0.38.0" dependencies = [ "anyhow", "async-channel", diff --git a/Cargo.toml b/Cargo.toml index acdf211876..951003b8ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij" -version = "0.37.2" +version = "0.38.0" authors = ["Aram Drevekenin "] edition = "2021" description = "A terminal workspace with batteries included" @@ -13,9 +13,9 @@ rust-version = "1.60" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -zellij-client = { path = "zellij-client/", version = "0.37.2" } -zellij-server = { path = "zellij-server/", version = "0.37.2" } -zellij-utils = { path = "zellij-utils/", version = "0.37.2" } +zellij-client = { path = "zellij-client/", version = "0.38.0" } +zellij-server = { path = "zellij-server/", version = "0.38.0" } +zellij-utils = { path = "zellij-utils/", version = "0.38.0" } thiserror = "1.0.40" names = { version = "0.14.0", default-features = false } log = "0.4.17" diff --git a/zellij-client/Cargo.toml b/zellij-client/Cargo.toml index 3cfd3f5d31..2a865dff58 100644 --- a/zellij-client/Cargo.toml +++ b/zellij-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij-client" -version = "0.37.2" +version = "0.38.0" authors = ["Kunal Mohan "] edition = "2021" description = "The client-side library for Zellij" @@ -14,7 +14,7 @@ serde = { version = "1.0", features = ["derive"] } url = { version = "2.2.2", features = ["serde"] } serde_yaml = "0.8" serde_json = "1.0" -zellij-utils = { path = "../zellij-utils/", version = "0.37.2" } +zellij-utils = { path = "../zellij-utils/", version = "0.38.0" } log = "0.4.17" [dev-dependencies] diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml index 8480c03d3b..74ba363c4b 100644 --- a/zellij-server/Cargo.toml +++ b/zellij-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij-server" -version = "0.37.2" +version = "0.38.0" authors = ["Kunal Mohan "] edition = "2021" description = "The server-side library for Zellij" @@ -21,7 +21,7 @@ url = "2.2.2" wasmer = "2.3.0" wasmer-wasi = "2.3.0" cassowary = "0.3.0" -zellij-utils = { path = "../zellij-utils/", version = "0.37.2" } +zellij-utils = { path = "../zellij-utils/", version = "0.38.0" } log = "0.4.17" typetag = "0.1.7" chrono = "0.4.19" diff --git a/zellij-tile-utils/Cargo.toml b/zellij-tile-utils/Cargo.toml index c39e92e7de..7b8530beb8 100644 --- a/zellij-tile-utils/Cargo.toml +++ b/zellij-tile-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij-tile-utils" -version = "0.37.2" +version = "0.38.0" authors = ["denis "] edition = "2021" description = "A utility library for Zellij plugins" diff --git a/zellij-tile/Cargo.toml b/zellij-tile/Cargo.toml index 5de407b2f2..caf1e13b83 100644 --- a/zellij-tile/Cargo.toml +++ b/zellij-tile/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij-tile" -version = "0.37.2" +version = "0.38.0" authors = ["Brooks J Rady "] edition = "2021" description = "A small client-side library for writing Zellij plugins" @@ -12,4 +12,4 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" strum = "0.20.0" strum_macros = "0.20.0" -zellij-utils = { path = "../zellij-utils/", version = "0.37.2" } +zellij-utils = { path = "../zellij-utils/", version = "0.38.0" } diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index 2877334dbe..cd5988a91c 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zellij-utils" -version = "0.37.2" +version = "0.38.0" authors = ["Kunal Mohan "] edition = "2021" description = "A utility library for Zellij client and server" From 62d6bdb5962a4870450da761d8ff418d9e490f0d Mon Sep 17 00:00:00 2001 From: har7an <99636919+har7an@users.noreply.github.com> Date: Sun, 16 Jul 2023 14:35:34 +0000 Subject: [PATCH 015/128] Fix colored pane frames in mirrored sessions (#2625) * server/panes/tiled: Fix colored frames in mirrored sessions. Colored frames were previously ignored because they were treated like floating panes when rendering tiled panes. * CHANGELOG: Add PR #2625 * server/tab/unit: Fix unit tests for server. --- CHANGELOG.md | 1 + zellij-server/src/panes/tiled_panes/mod.rs | 2 +- ...gration_tests__base_layout_is_included_in_swap_layouts.snap | 3 +-- ...ests__layout_with_plugins_and_commands_swaped_properly.snap | 3 +-- ...not_including_command_panes_present_in_existing_layout.snap | 3 +-- ..._not_including_plugin_panes_present_in_existing_layout.snap | 3 +-- 6 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d30f44e9..fe3ea7e13a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * fix(tab-bar,compact-bar): tab switching with mouse sometimes not working (https://github.com/zellij-org/zellij/pull/2587) * feat(status-bar): supermode to prevent colliding keybindings (https://github.com/zellij-org/zellij/pull/2619) * fix(rendering): occasional glitches while resizing (https://github.com/zellij-org/zellij/pull/2621) +* fix(rendering): colored paneframes in mirrored sessions (https://github.com/zellij-org/zellij/pull/2625) ## [0.37.2] - 2023-06-20 * hotfix: include theme files into binary (https://github.com/zellij-org/zellij/pull/2566) diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index 424053405b..f0a6e8436a 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -623,7 +623,7 @@ impl TiledPanes { { self.connected_clients.borrow().iter().copied().collect() }; let multiple_users_exist_in_session = { self.connected_clients_in_app.borrow().len() > 1 }; let mut client_id_to_boundaries: HashMap = HashMap::new(); - let active_panes = if self.session_is_mirrored || floating_panes_are_visible { + let active_panes = if floating_panes_are_visible { HashMap::new() } else { self.active_panes diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__base_layout_is_included_in_swap_layouts.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__base_layout_is_included_in_swap_layouts.snap index 5fbf2847ac..97692e660b 100644 --- a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__base_layout_is_included_in_swap_layouts.snap +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__base_layout_is_included_in_swap_layouts.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/tab/./unit/tab_integration_tests.rs -assertion_line: 4762 expression: snapshot --- 00 (C): I am a tab bar @@ -20,7 +19,7 @@ expression: snapshot 14 (C): │ ││ ││ │ 15 (C): │ ││ ││ │ 16 (C): │ ││ ││ │ -17 (C): └───────────────────────────────────────┘└──────────────────────────────────────┘└──────────────────────────────────────┘ +17 (C): └───────────────────────────────────────┘└──────────────────────────────────────┘└─ to run, to exit ───┘ 18 (C): I am a 19 (C): status bar diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__layout_with_plugins_and_commands_swaped_properly.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__layout_with_plugins_and_commands_swaped_properly.snap index c02cd98ca9..4c31fe251d 100644 --- a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__layout_with_plugins_and_commands_swaped_properly.snap +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__layout_with_plugins_and_commands_swaped_properly.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/tab/./unit/tab_integration_tests.rs -assertion_line: 4992 expression: snapshot --- 00 (C): I am a @@ -10,7 +9,7 @@ expression: snapshot 04 (C): │ Waiting to run: command1 │ 05 (C): │ │ 06 (C): │ to run, to exit │ -07 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +07 (C): └─ to run, to exit ────────────────────────────────────────────────────────────────────────────────────┘ 08 (C): ┌ command2 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 09 (C): │ │ 10 (C): │ Waiting to run: command2 │ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__swap_layouts_not_including_command_panes_present_in_existing_layout.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__swap_layouts_not_including_command_panes_present_in_existing_layout.snap index dd0cbbd721..edac6ec4bb 100644 --- a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__swap_layouts_not_including_command_panes_present_in_existing_layout.snap +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__swap_layouts_not_including_command_panes_present_in_existing_layout.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/tab/./unit/tab_integration_tests.rs -assertion_line: 4904 expression: snapshot --- 00 (C): I am a @@ -16,7 +15,7 @@ expression: snapshot 10 (C): │ Waiting to run: command1 │ 11 (C): │ │ 12 (C): │ to run, to exit │ -13 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +13 (C): └─ to run, to exit ────────────────────────────────────────────────────────────────────────────────────┘ 14 (C): ┌ command2 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 15 (C): │ Waiting to run: command2 │ 16 (C): │ │ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__swap_layouts_not_including_plugin_panes_present_in_existing_layout.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__swap_layouts_not_including_plugin_panes_present_in_existing_layout.snap index 258ea58bea..d5366c733b 100644 --- a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__swap_layouts_not_including_plugin_panes_present_in_existing_layout.snap +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__swap_layouts_not_including_plugin_panes_present_in_existing_layout.snap @@ -1,6 +1,5 @@ --- source: zellij-server/src/tab/./unit/tab_integration_tests.rs -assertion_line: 5035 expression: snapshot --- 00 (C): ┌ Pane #2 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -10,7 +9,7 @@ expression: snapshot 04 (C): │ Waiting to run: command1 │ 05 (C): │ │ 06 (C): │ to run, to exit │ -07 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +07 (C): └─ to run, to exit ────────────────────────────────────────────────────────────────────────────────────┘ 08 (C): ┌ command2 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 09 (C): │ │ 10 (C): │ Waiting to run: command2 │ From 6ff71e1b18035dc9dd683d1fd331320b760b77e7 Mon Sep 17 00:00:00 2001 From: Kyle Sutherland-Cash Date: Tue, 18 Jul 2023 18:12:51 +0100 Subject: [PATCH 016/128] fix(sessions): use custom lists of adjectives and nouns for generating session names (#2122) * Create custom lists of adjectives and nouns for generating session names * move word lists to const slices * add logic to retry name generation * refactor - reuse the name generator - iterator instead of for loop --------- Co-authored-by: Thomas Linford --- src/commands.rs | 25 +++++++- src/sessions.rs | 148 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 3 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 84677b06b4..1910dd26d8 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -2,7 +2,7 @@ use dialoguer::Confirm; use std::{fs::File, io::prelude::*, path::PathBuf, process}; use crate::sessions::{ - assert_session, assert_session_ne, get_active_session, get_sessions, + assert_session, assert_session_ne, get_active_session, get_name_generator, get_sessions, get_sessions_sorted_by_mtime, kill_session as kill_session_impl, match_session_name, print_sessions, print_sessions_with_index, session_exists, ActiveSession, SessionNameMatch, }; @@ -93,7 +93,7 @@ pub(crate) fn start_server(path: PathBuf, debug: bool) { } fn create_new_client() -> ClientInfo { - ClientInfo::New(names::Generator::default().next().unwrap()) + ClientInfo::New(generate_unique_session_name()) } fn find_indexed_session( @@ -449,7 +449,7 @@ pub(crate) fn start_client(opts: CliArgs) { process::exit(0); } - let session_name = names::Generator::default().next().unwrap(); + let session_name = generate_unique_session_name(); start_client_plan(session_name.clone()); start_client_impl( Box::new(os_input), @@ -462,3 +462,22 @@ pub(crate) fn start_client(opts: CliArgs) { } } } + +fn generate_unique_session_name() -> String { + let sessions = get_sessions(); + let Ok(sessions) = sessions else { + eprintln!("Failed to list existing sessions: {:?}", sessions); + process::exit(1); + }; + + let name = get_name_generator() + .take(1000) + .find(|name| !sessions.contains(name)); + + if let Some(name) = name { + return name; + } else { + eprintln!("Failed to generate a unique session name, giving up"); + process::exit(1); + } +} diff --git a/src/sessions.rs b/src/sessions.rs index 9c7de37e9f..910bd333b4 100644 --- a/src/sessions.rs +++ b/src/sessions.rs @@ -219,3 +219,151 @@ pub(crate) fn assert_session_ne(name: &str) { }; process::exit(1); } + +/// Create a new random name generator +/// +/// Used to provide a memorable handle for a session when users don't specify a session name when the session is +/// created. +/// +/// Uses the list of adjectives and nouns defined below, with the intention of avoiding unfortunate +/// and offensive combinations. Care should be taken when adding or removing to either list due to the birthday paradox/ +/// hash collisions, e.g. with 4096 unique names, the likelihood of a collision in 10 session names is 1%. +pub(crate) fn get_name_generator() -> impl Iterator { + names::Generator::new(&ADJECTIVES, &NOUNS, names::Name::Plain) +} + +const ADJECTIVES: &[&'static str] = &[ + "adamant", + "adept", + "adventurous", + "arcadian", + "auspicious", + "awesome", + "blossoming", + "brave", + "charming", + "chatty", + "circular", + "considerate", + "cubic", + "curious", + "delighted", + "didactic", + "diligent", + "effulgent", + "erudite", + "excellent", + "exquisite", + "fabulous", + "fascinating", + "friendly", + "glowing", + "gracious", + "gregarious", + "hopeful", + "implacable", + "inventive", + "joyous", + "judicious", + "jumping", + "kind", + "likable", + "loyal", + "lucky", + "marvellous", + "mellifluous", + "nautical", + "oblong", + "outstanding", + "polished", + "polite", + "profound", + "quadratic", + "quiet", + "rectangular", + "remarkable", + "rusty", + "sensible", + "sincere", + "sparkling", + "splendid", + "stellar", + "tenacious", + "tremendous", + "triangular", + "undulating", + "unflappable", + "unique", + "verdant", + "vitreous", + "wise", + "zippy", +]; + +const NOUNS: &[&'static str] = &[ + "aardvark", + "accordion", + "apple", + "apricot", + "bee", + "brachiosaur", + "cactus", + "capsicum", + "clarinet", + "cowbell", + "crab", + "cuckoo", + "cymbal", + "diplodocus", + "donkey", + "drum", + "duck", + "echidna", + "elephant", + "foxglove", + "galaxy", + "glockenspiel", + "goose", + "hill", + "horse", + "iguanadon", + "jellyfish", + "kangaroo", + "lake", + "lemon", + "lemur", + "magpie", + "megalodon", + "mountain", + "mouse", + "muskrat", + "newt", + "oboe", + "ocelot", + "orange", + "panda", + "peach", + "pepper", + "petunia", + "pheasant", + "piano", + "pigeon", + "platypus", + "quasar", + "rhinoceros", + "river", + "rustacean", + "salamander", + "sitar", + "stegosaurus", + "tambourine", + "tiger", + "tomato", + "triceratops", + "ukulele", + "viola", + "weasel", + "xylophone", + "yak", + "zebra", +]; From 86e8a65395d5ce11f2c59f1ca170ee5dd9b1b0f3 Mon Sep 17 00:00:00 2001 From: Thomas Linford Date: Tue, 18 Jul 2023 19:16:29 +0200 Subject: [PATCH 017/128] docs(changelog): generate session names with custom words list --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe3ea7e13a..db02f9d626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * feat(status-bar): supermode to prevent colliding keybindings (https://github.com/zellij-org/zellij/pull/2619) * fix(rendering): occasional glitches while resizing (https://github.com/zellij-org/zellij/pull/2621) * fix(rendering): colored paneframes in mirrored sessions (https://github.com/zellij-org/zellij/pull/2625) +* fix(sessions): use custom lists of adjectives and nouns for generating session names (https://github.com/zellij-org/zellij/pull/2122) ## [0.37.2] - 2023-06-20 * hotfix: include theme files into binary (https://github.com/zellij-org/zellij/pull/2566) From ce0b59421f4a31342acfa54101b577281463b5c8 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 25 Jul 2023 10:04:12 +0200 Subject: [PATCH 018/128] feat(plugins): make plugins configurable (#2646) * work * make every plugin entry point configurable * make integration tests pass * make e2e tests pass * add test for plugin configuration * add test snapshot * add plugin config parsing test * cleanups * style(fmt): rustfmt * style(comment): remove commented code --- default-plugins/compact-bar/src/main.rs | 3 +- .../fixture-plugin-for-tests/src/main.rs | 8 +- default-plugins/status-bar/src/main.rs | 8 +- default-plugins/strider/src/main.rs | 3 +- default-plugins/tab-bar/src/main.rs | 3 +- src/main.rs | 1 + zellij-server/src/plugins/plugin_loader.rs | 9 +- .../src/plugins/unit/plugin_tests.rs | 138 +++++++++++++++++- ..._tests__send_configuration_to_plugins.snap | 19 +++ ..._plugin_tests__start_or_reload_plugin.snap | 5 +- zellij-server/src/plugins/zellij_exports.rs | 5 +- zellij-server/src/screen.rs | 58 ++++---- zellij-server/src/unit/screen_tests.rs | 6 + ...end_cli_launch_or_focus_plugin_action.snap | 5 +- zellij-tile/src/lib.rs | 9 +- zellij-utils/src/cli.rs | 8 +- zellij-utils/src/input/actions.rs | 29 ++-- zellij-utils/src/input/config.rs | 4 + zellij-utils/src/input/layout.rs | 29 ++++ zellij-utils/src/input/plugins.rs | 6 +- zellij-utils/src/input/unit/layout_test.rs | 25 ++++ ...ad_swap_layouts_from_a_different_file.snap | 20 ++- zellij-utils/src/kdl/kdl_layout_parser.rs | 65 ++++++++- zellij-utils/src/kdl/mod.rs | 107 +++++++++----- ...efault_config_with_no_cli_arguments-2.snap | 49 +++++++ ..._default_config_with_no_cli_arguments.snap | 13 ++ ...out_env_vars_override_config_env_vars.snap | 13 ++ ...out_keybinds_override_config_keybinds.snap | 13 ++ ...ayout_plugins_override_config_plugins.snap | 16 ++ ..._layout_themes_override_config_themes.snap | 13 ++ ..._ui_config_overrides_config_ui_config.snap | 13 ++ 31 files changed, 605 insertions(+), 98 deletions(-) create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap diff --git a/default-plugins/compact-bar/src/main.rs b/default-plugins/compact-bar/src/main.rs index 40fa65f52a..18c9f12b4d 100644 --- a/default-plugins/compact-bar/src/main.rs +++ b/default-plugins/compact-bar/src/main.rs @@ -2,6 +2,7 @@ mod line; mod tab; use std::cmp::{max, min}; +use std::collections::BTreeMap; use std::convert::TryInto; use tab::get_tab_to_focus; @@ -30,7 +31,7 @@ static ARROW_SEPARATOR: &str = ""; register_plugin!(State); impl ZellijPlugin for State { - fn load(&mut self) { + fn load(&mut self, configuration: BTreeMap) { set_selectable(false); subscribe(&[ EventType::TabUpdate, diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 20457f13fd..074aa1a4d0 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use zellij_tile::prelude::*; // This is a fixture plugin used only for tests in Zellij @@ -9,6 +10,7 @@ use zellij_tile::prelude::*; struct State { received_events: Vec, received_payload: Option, + configuration: BTreeMap, } #[derive(Default, Serialize, Deserialize)] @@ -35,7 +37,8 @@ register_plugin!(State); register_worker!(TestWorker, test_worker, TEST_WORKER); impl ZellijPlugin for State { - fn load(&mut self) { + fn load(&mut self, configuration: BTreeMap) { + self.configuration = configuration; subscribe(&[ EventType::InputReceived, EventType::Key, @@ -210,6 +213,9 @@ impl ZellijPlugin for State { Key::Ctrl('x') => { rename_tab(1, "new tab name"); }, + Key::Ctrl('z') => { + go_to_tab_name(&format!("{:?}", self.configuration)); + }, _ => {}, }, Event::CustomMessage(message, payload) => { diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs index ee14e45d54..d768b35ed8 100644 --- a/default-plugins/status-bar/src/main.rs +++ b/default-plugins/status-bar/src/main.rs @@ -8,6 +8,7 @@ use ansi_term::{ Style, }; +use std::collections::BTreeMap; use std::fmt::{Display, Error, Formatter}; use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::*; @@ -196,7 +197,7 @@ fn color_elements(palette: Palette, different_color_alternates: bool) -> Colored } impl ZellijPlugin for State { - fn load(&mut self) { + fn load(&mut self, configuration: BTreeMap) { // TODO: Should be able to choose whether to use the cache through config. self.tip_name = get_cached_tip_name(); set_selectable(false); @@ -207,7 +208,10 @@ impl ZellijPlugin for State { EventType::InputReceived, EventType::SystemClipboardFailure, ]); - self.supermode = false; // TODO: from config + self.supermode = configuration + .get("supermode") + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(false); self.standby_mode = InputMode::Pane; if self.supermode { switch_to_input_mode(&InputMode::Locked); // supermode should start locked (TODO: only diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs index 24d443d10c..35ff92ea77 100644 --- a/default-plugins/strider/src/main.rs +++ b/default-plugins/strider/src/main.rs @@ -6,6 +6,7 @@ use search::{FileContentsWorker, FileNameWorker, MessageToSearch, ResultsOfSearc use serde::{Deserialize, Serialize}; use serde_json; use state::{refresh_directory, FsEntry, State}; +use std::collections::BTreeMap; use std::{cmp::min, time::Instant}; use zellij_tile::prelude::*; @@ -18,7 +19,7 @@ register_worker!( ); impl ZellijPlugin for State { - fn load(&mut self) { + fn load(&mut self, configuration: BTreeMap) { refresh_directory(self); self.search_state.loading = true; subscribe(&[ diff --git a/default-plugins/tab-bar/src/main.rs b/default-plugins/tab-bar/src/main.rs index 05fdd5608d..fc67ef918e 100644 --- a/default-plugins/tab-bar/src/main.rs +++ b/default-plugins/tab-bar/src/main.rs @@ -2,6 +2,7 @@ mod line; mod tab; use std::cmp::{max, min}; +use std::collections::BTreeMap; use std::convert::TryInto; use tab::get_tab_to_focus; @@ -30,7 +31,7 @@ static ARROW_SEPARATOR: &str = ""; register_plugin!(State); impl ZellijPlugin for State { - fn load(&mut self) { + fn load(&mut self, configuration: BTreeMap) { set_selectable(false); subscribe(&[ EventType::TabUpdate, diff --git a/src/main.rs b/src/main.rs index e3737b19ef..f32d76cd85 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,6 +39,7 @@ fn main() { name, close_on_exit, start_suspended, + configuration: None, }; commands::send_action_to_session(command_cli_action, opts.session, config); std::process::exit(0); diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs index 324b540d54..73957cf7cc 100644 --- a/zellij-server/src/plugins/plugin_loader.rs +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -1,6 +1,6 @@ use crate::plugins::plugin_map::{PluginEnv, PluginMap, RunningPlugin, Subscriptions}; use crate::plugins::plugin_worker::{plugin_worker, RunningWorker}; -use crate::plugins::zellij_exports::{wasi_read_string, zellij_exports}; +use crate::plugins::zellij_exports::{wasi_read_string, wasi_write_object, zellij_exports}; use crate::plugins::PluginId; use highway::{HighwayHash, PortableHash}; use log::info; @@ -723,7 +723,14 @@ impl<'a> PluginLoader<'a> { } } start_function.call(&[]).with_context(err_context)?; + + wasi_write_object( + &plugin_env.wasi_env, + &self.plugin.userspace_configuration.inner(), + ) + .with_context(err_context)?; load_function.call(&[]).with_context(err_context)?; + display_loading_stage!( indicate_starting_plugin_success, self.loading_indication, diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 3ec8485b03..663ca94eec 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -2,12 +2,13 @@ use super::plugin_thread_main; use crate::screen::ScreenInstruction; use crate::{channels::SenderWithContext, thread_bus::Bus, ServerInstruction}; use insta::assert_snapshot; +use std::collections::BTreeMap; use std::path::PathBuf; use tempfile::tempdir; use wasmer::Store; use zellij_utils::data::{Event, Key, PluginCapabilities}; use zellij_utils::errors::ErrorContext; -use zellij_utils::input::layout::{Layout, RunPlugin, RunPluginLocation}; +use zellij_utils::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation}; use zellij_utils::input::plugins::PluginsConfig; use zellij_utils::ipc::ClientAttributes; use zellij_utils::lazy_static::lazy_static; @@ -349,6 +350,7 @@ pub fn load_new_plugin_from_hd() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -407,6 +409,7 @@ pub fn plugin_workers() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -468,6 +471,7 @@ pub fn plugin_workers_persist_state() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -541,6 +545,7 @@ pub fn can_subscribe_to_hd_events() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -605,6 +610,7 @@ pub fn switch_to_mode_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -666,6 +672,7 @@ pub fn new_tabs_with_layout_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -741,6 +748,7 @@ pub fn new_tab_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -802,6 +810,7 @@ pub fn go_to_next_tab_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -862,6 +871,7 @@ pub fn go_to_previous_tab_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -922,6 +932,7 @@ pub fn resize_focused_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -982,6 +993,7 @@ pub fn resize_focused_pane_with_direction_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1042,6 +1054,7 @@ pub fn focus_next_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1102,6 +1115,7 @@ pub fn focus_previous_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1162,6 +1176,7 @@ pub fn move_focus_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1222,6 +1237,7 @@ pub fn move_focus_or_tab_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1282,6 +1298,7 @@ pub fn edit_scrollback_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1342,6 +1359,7 @@ pub fn write_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1402,6 +1420,7 @@ pub fn write_chars_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1462,6 +1481,7 @@ pub fn toggle_tab_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1522,6 +1542,7 @@ pub fn move_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1582,6 +1603,7 @@ pub fn move_pane_with_direction_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1642,6 +1664,7 @@ pub fn clear_screen_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1702,6 +1725,7 @@ pub fn scroll_up_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1762,6 +1786,7 @@ pub fn scroll_down_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1822,6 +1847,7 @@ pub fn scroll_to_top_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1882,6 +1908,7 @@ pub fn scroll_to_bottom_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -1942,6 +1969,7 @@ pub fn page_scroll_up_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2002,6 +2030,7 @@ pub fn page_scroll_down_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2062,6 +2091,7 @@ pub fn toggle_focus_fullscreen_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2122,6 +2152,7 @@ pub fn toggle_pane_frames_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2182,6 +2213,7 @@ pub fn toggle_pane_embed_or_eject_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2242,6 +2274,7 @@ pub fn undo_rename_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2302,6 +2335,7 @@ pub fn close_focus_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2362,6 +2396,7 @@ pub fn toggle_active_tab_sync_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2422,6 +2457,7 @@ pub fn close_focused_tab_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2482,6 +2518,7 @@ pub fn undo_rename_tab_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2542,6 +2579,7 @@ pub fn previous_swap_layout_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2602,6 +2640,7 @@ pub fn next_swap_layout_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2662,6 +2701,7 @@ pub fn go_to_tab_name_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2722,6 +2762,7 @@ pub fn focus_or_create_tab_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2782,6 +2823,7 @@ pub fn go_to_tab() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2842,6 +2884,7 @@ pub fn start_or_reload_plugin() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2902,6 +2945,7 @@ pub fn quit_zellij_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -2962,6 +3006,7 @@ pub fn detach_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3022,6 +3067,7 @@ pub fn open_file_floating_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3082,6 +3128,7 @@ pub fn open_file_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3142,6 +3189,7 @@ pub fn open_file_with_line_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3202,6 +3250,7 @@ pub fn open_file_with_line_floating_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3262,6 +3311,7 @@ pub fn open_terminal_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3322,6 +3372,7 @@ pub fn open_terminal_floating_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3382,6 +3433,7 @@ pub fn open_command_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3442,6 +3494,7 @@ pub fn open_command_pane_floating_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3502,6 +3555,7 @@ pub fn switch_to_tab_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3562,6 +3616,7 @@ pub fn hide_self_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3622,6 +3677,7 @@ pub fn show_self_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3682,6 +3738,7 @@ pub fn close_terminal_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3742,6 +3799,7 @@ pub fn close_plugin_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3802,6 +3860,7 @@ pub fn focus_terminal_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3862,6 +3921,7 @@ pub fn focus_plugin_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3922,6 +3982,7 @@ pub fn rename_terminal_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -3982,6 +4043,7 @@ pub fn rename_plugin_pane_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -4042,6 +4104,7 @@ pub fn rename_tab_plugin_command() { let run_plugin = RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), }; let tab_index = 1; let client_id = 1; @@ -4088,3 +4151,76 @@ pub fn rename_tab_plugin_command() { .clone(); assert_snapshot!(format!("{:#?}", new_tab_event)); } + +#[test] +#[ignore] +pub fn send_configuration_to_plugins() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let (plugin_thread_sender, screen_receiver, mut teardown) = + create_plugin_thread(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let mut configuration = BTreeMap::new(); + configuration.insert( + "fake_config_key_1".to_owned(), + "fake_config_value_1".to_owned(), + ); + configuration.insert( + "fake_config_key_2".to_owned(), + "fake_config_value_2".to_owned(), + ); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: PluginUserConfiguration::new(configuration), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::GoToTabName, + screen_receiver, + 1 + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + plugin_title, + run_plugin, + tab_index, + client_id, + size, + )); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('z')), // this triggers the enent in the fixture plugin + )])); + std::thread::sleep(std::time::Duration::from_millis(100)); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + // here we make sure we received a rename_tab event with the title being the stringified + // (Debug) configuration we sent to the fixture plugin to make sure it got there properly + + let go_to_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::GoToTabName(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", go_to_tab_event)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap new file mode 100644 index 0000000000..01cb4b6b4d --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap @@ -0,0 +1,19 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 4220 +expression: "format!(\"{:#?}\", go_to_tab_event)" +--- +Some( + GoToTabName( + "{\"fake_config_key_1\": \"fake_config_value_1\", \"fake_config_key_2\": \"fake_config_value_2\"}\n\r", + ( + [], + [], + ), + None, + false, + Some( + 1, + ), + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap index 72364a88e0..05002124ab 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__start_or_reload_plugin.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 2889 +assertion_line: 2931 expression: "format!(\"{:#?}\", new_tab_event)" --- Some( @@ -10,6 +10,9 @@ Some( location: File( "/path/to/my/plugin.wasm", ), + configuration: PluginUserConfiguration( + {}, + ), }, None, ), diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index e4d85a1160..c73068470a 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -5,7 +5,7 @@ use crate::route::route_action; use log::{debug, warn}; use serde::{de::DeserializeOwned, Serialize}; use std::{ - collections::HashSet, + collections::{BTreeMap, HashSet}, path::PathBuf, process, sync::{Arc, Mutex}, @@ -26,7 +26,7 @@ use zellij_utils::{ input::{ actions::Action, command::{RunCommand, RunCommandAction, TerminalAction}, - layout::{Layout, RunPlugin, RunPluginLocation}, + layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation}, plugins::PluginType, }, serde, @@ -976,6 +976,7 @@ fn host_start_or_reload_plugin(env: &ForeignFunctionEnv) { let run_plugin = RunPlugin { location: run_plugin_location, _allow_exec_host_cmd: false, + configuration: PluginUserConfiguration::new(BTreeMap::new()), // TODO: allow passing configuration }; let action = Action::StartOrReloadPlugin(run_plugin); apply_action!(action, error_msg, env); diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index fc86b4faef..d07be93c26 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -14,8 +14,8 @@ use zellij_utils::pane_size::{PaneGeom, Size, SizeInPixels}; use zellij_utils::{ input::command::TerminalAction, input::layout::{ - FloatingPaneLayout, Run, RunPlugin, RunPluginLocation, SwapFloatingLayout, SwapTiledLayout, - TiledPaneLayout, + FloatingPaneLayout, PluginUserConfiguration, Run, RunPlugin, RunPluginLocation, + SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }, persistence, position::Position, @@ -260,11 +260,11 @@ pub enum ScreenInstruction { PreviousSwapLayout(ClientId), NextSwapLayout(ClientId), QueryTabNames(ClientId), - NewTiledPluginPane(RunPluginLocation, Option, ClientId), // Option is + NewTiledPluginPane(RunPlugin, Option, ClientId), // Option is // optional pane title - NewFloatingPluginPane(RunPluginLocation, Option, ClientId), // Option is an - StartOrReloadPluginPane(RunPlugin, Option), + NewFloatingPluginPane(RunPlugin, Option, ClientId), // Option is an // optional pane title + StartOrReloadPluginPane(RunPlugin, Option), AddPlugin( Option, // should_float RunPlugin, @@ -2654,14 +2654,10 @@ pub(crate) fn screen_thread_main( .senders .send_to_server(ServerInstruction::Log(tab_names, client_id))?; }, - ScreenInstruction::NewTiledPluginPane(run_plugin_location, pane_title, client_id) => { + ScreenInstruction::NewTiledPluginPane(run_plugin, pane_title, client_id) => { let tab_index = screen.active_tab_indices.values().next().unwrap_or(&1); let size = Size::default(); let should_float = Some(false); - let run_plugin = RunPlugin { - _allow_exec_host_cmd: false, - location: run_plugin_location, - }; screen.bus.senders.send_to_plugin(PluginInstruction::Load( should_float, pane_title, @@ -2671,28 +2667,26 @@ pub(crate) fn screen_thread_main( size, ))?; }, - ScreenInstruction::NewFloatingPluginPane( - run_plugin_location, - pane_title, - client_id, - ) => { - let tab_index = screen.active_tab_indices.values().next().unwrap(); // TODO: no - // unwrap and - // better - let size = Size::default(); // TODO: ??? - let should_float = Some(true); - let run_plugin = RunPlugin { - _allow_exec_host_cmd: false, - location: run_plugin_location, - }; - screen.bus.senders.send_to_plugin(PluginInstruction::Load( - should_float, - pane_title, - run_plugin, - *tab_index, - client_id, - size, - ))?; + ScreenInstruction::NewFloatingPluginPane(run_plugin, pane_title, client_id) => { + match screen.active_tab_indices.values().next() { + Some(tab_index) => { + let size = Size::default(); + let should_float = Some(true); + screen.bus.senders.send_to_plugin(PluginInstruction::Load( + should_float, + pane_title, + run_plugin, + *tab_index, + client_id, + size, + ))?; + }, + None => { + log::error!( + "Could not find an active tab - is there at least 1 connected user?" + ); + }, + } }, ScreenInstruction::StartOrReloadPluginPane(run_plugin, pane_title) => { let tab_index = screen.active_tab_indices.values().next().unwrap_or(&1); diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 1040062b15..8a683edc0f 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -1854,6 +1854,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() { name: None, close_on_exit: false, start_suspended: false, + configuration: None, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -1890,6 +1891,7 @@ pub fn send_cli_new_pane_action_with_split_direction() { name: None, close_on_exit: false, start_suspended: false, + configuration: None, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -1926,6 +1928,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() { name: None, close_on_exit: false, start_suspended: false, + configuration: None, }; send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2550,6 +2553,7 @@ pub fn send_cli_launch_or_focus_plugin_action() { let cli_action = CliAction::LaunchOrFocusPlugin { floating: true, url: url::Url::parse("file:/path/to/fake/plugin").unwrap(), + configuration: Default::default(), }; send_cli_action_to_server(&session_metadata, cli_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be @@ -2583,6 +2587,7 @@ pub fn send_cli_launch_or_focus_plugin_action_when_plugin_is_already_loaded() { run: Some(Run::Plugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from("/path/to/fake/plugin")), + configuration: Default::default(), })), ..Default::default() }; @@ -2605,6 +2610,7 @@ pub fn send_cli_launch_or_focus_plugin_action_when_plugin_is_already_loaded() { let cli_action = CliAction::LaunchOrFocusPlugin { floating: true, url: url::Url::parse("file:/path/to/fake/plugin").unwrap(), + configuration: Default::default(), }; send_cli_action_to_server(&session_metadata, cli_action, client_id); std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap index 0751fa5f1c..d6420c3fcf 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_launch_or_focus_plugin_action.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 2758 +assertion_line: 2572 expression: "format!(\"{:#?}\", plugin_load_instruction)" --- Some( @@ -14,6 +14,9 @@ Some( location: File( "/path/to/fake/plugin", ), + configuration: PluginUserConfiguration( + {}, + ), }, 0, 1, diff --git a/zellij-tile/src/lib.rs b/zellij-tile/src/lib.rs index 98f141942c..7bcaf47383 100644 --- a/zellij-tile/src/lib.rs +++ b/zellij-tile/src/lib.rs @@ -19,6 +19,7 @@ pub mod prelude; pub mod shim; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use zellij_utils::data::Event; /// This trait should be implemented - once per plugin - on a struct (normally representing the @@ -27,7 +28,7 @@ use zellij_utils::data::Event; #[allow(unused_variables)] pub trait ZellijPlugin: Default { /// Will be called when the plugin is loaded, this is a good place to [`subscribe`](shim::subscribe) to events that are interesting for this plugin. - fn load(&mut self) {} + fn load(&mut self, configuration: BTreeMap) {} /// Will be called with an [`Event`](prelude::Event) if the plugin is subscribed to said event. /// If the plugin returns `true` from this function, Zellij will know it should be rendered and call its `render` function. fn update(&mut self, event: Event) -> bool { @@ -103,7 +104,11 @@ macro_rules! register_plugin { #[no_mangle] fn load() { STATE.with(|state| { - state.borrow_mut().load(); + let configuration = $crate::shim::object_from_stdin() + .context($crate::PLUGIN_MISMATCH) + .to_stdout() + .unwrap(); + state.borrow_mut().load(configuration); }); } diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 953d520e6d..362686f04b 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -2,7 +2,7 @@ use crate::data::{Direction, InputMode, Resize}; use crate::setup::Setup; use crate::{ consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV}, - input::options::CliOptions, + input::{layout::PluginUserConfiguration, options::CliOptions}, }; use clap::{Parser, Subcommand}; use serde::{Deserialize, Serialize}; @@ -297,6 +297,8 @@ pub enum CliAction { requires("command") )] start_suspended: bool, + #[clap(short, long, value_parser)] + configuration: Option, }, /// Open the specified file in a new zellij pane with your default EDITOR Edit { @@ -381,10 +383,14 @@ pub enum CliAction { QueryTabNames, StartOrReloadPlugin { url: String, + #[clap(short, long, value_parser)] + configuration: Option, }, LaunchOrFocusPlugin { #[clap(short, long, value_parser)] floating: bool, url: Url, + #[clap(short, long, value_parser)] + configuration: Option, }, } diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index d4f69652e2..746624a890 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -232,8 +232,8 @@ pub enum Action { /// Query all tab names QueryTabNames, /// Open a new tiled (embedded, non-floating) plugin pane - NewTiledPluginPane(RunPluginLocation, Option), // String is an optional name - NewFloatingPluginPane(RunPluginLocation, Option), // String is an optional name + NewTiledPluginPane(RunPlugin, Option), // String is an optional name + NewFloatingPluginPane(RunPlugin, Option), // String is an optional name StartOrReloadPlugin(RunPlugin), CloseTerminalPane(u32), ClosePluginPane(u32), @@ -297,21 +297,24 @@ impl Action { name, close_on_exit, start_suspended, + configuration, } => { let current_dir = get_current_dir(); let cwd = cwd .map(|cwd| current_dir.join(cwd)) .or_else(|| Some(current_dir)); + let user_configuration = configuration.unwrap_or_default(); if let Some(plugin) = plugin { + let location = RunPluginLocation::parse(&plugin, cwd) + .map_err(|e| format!("Failed to parse plugin loction {plugin}: {}", e))?; + let plugin = RunPlugin { + _allow_exec_host_cmd: false, + location, + configuration: user_configuration, + }; if floating { - let plugin = RunPluginLocation::parse(&plugin, cwd).map_err(|e| { - format!("Failed to parse plugin loction {plugin}: {}", e) - })?; Ok(vec![Action::NewFloatingPluginPane(plugin, name)]) } else { - let plugin = RunPluginLocation::parse(&plugin, cwd).map_err(|e| { - format!("Failed to parse plugin location {plugin}: {}", e) - })?; // it is intentional that a new tiled plugin pane cannot include a // direction // this is because the cli client opening a tiled plugin pane is a @@ -485,23 +488,29 @@ impl Action { CliAction::PreviousSwapLayout => Ok(vec![Action::PreviousSwapLayout]), CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]), CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]), - CliAction::StartOrReloadPlugin { url } => { + CliAction::StartOrReloadPlugin { url, configuration } => { let current_dir = get_current_dir(); let run_plugin_location = RunPluginLocation::parse(&url, Some(current_dir)) .map_err(|e| format!("Failed to parse plugin location: {}", e))?; let run_plugin = RunPlugin { location: run_plugin_location, _allow_exec_host_cmd: false, + configuration: configuration.unwrap_or_default(), }; Ok(vec![Action::StartOrReloadPlugin(run_plugin)]) }, - CliAction::LaunchOrFocusPlugin { url, floating } => { + CliAction::LaunchOrFocusPlugin { + url, + floating, + configuration, + } => { let current_dir = get_current_dir(); let run_plugin_location = RunPluginLocation::parse(url.as_str(), Some(current_dir)) .map_err(|e| format!("Failed to parse plugin location: {}", e))?; let run_plugin = RunPlugin { location: run_plugin_location, _allow_exec_host_cmd: false, + configuration: configuration.unwrap_or_default(), }; Ok(vec![Action::LaunchOrFocusPlugin(run_plugin, floating)]) }, diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index a6816ff43d..8296407505 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -600,6 +600,7 @@ mod config_test { run: PluginType::Pane(None), location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), _allow_exec_host_cmd: false, + userspace_configuration: Default::default(), }, ); expected_plugin_configuration.insert( @@ -609,6 +610,7 @@ mod config_test { run: PluginType::Pane(None), location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), _allow_exec_host_cmd: false, + userspace_configuration: Default::default(), }, ); expected_plugin_configuration.insert( @@ -618,6 +620,7 @@ mod config_test { run: PluginType::Pane(None), location: RunPluginLocation::Zellij(PluginTag::new("strider")), _allow_exec_host_cmd: true, + userspace_configuration: Default::default(), }, ); expected_plugin_configuration.insert( @@ -627,6 +630,7 @@ mod config_test { run: PluginType::Pane(None), location: RunPluginLocation::Zellij(PluginTag::new("compact-bar")), _allow_exec_host_cmd: false, + userspace_configuration: Default::default(), }, ); assert_eq!( diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 01247a8788..343431acf3 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -213,6 +213,35 @@ pub struct RunPlugin { #[serde(default)] pub _allow_exec_host_cmd: bool, pub location: RunPluginLocation, + pub configuration: PluginUserConfiguration, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct PluginUserConfiguration(BTreeMap); + +impl PluginUserConfiguration { + pub fn new(configuration: BTreeMap) -> Self { + PluginUserConfiguration(configuration) + } + pub fn inner(&self) -> &BTreeMap { + &self.0 + } +} + +impl FromStr for PluginUserConfiguration { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + let mut ret = BTreeMap::new(); + let configs = s.split(','); + for config in configs { + let mut config = config.split('='); + let key = config.next().ok_or("invalid configuration key")?.to_owned(); + let value = config.map(|c| c.to_owned()).collect::>().join("="); + ret.insert(key, value); + } + Ok(PluginUserConfiguration(ret)) + } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs index 47cc3aa74d..f12aba2a20 100644 --- a/zellij-utils/src/input/plugins.rs +++ b/zellij-utils/src/input/plugins.rs @@ -8,7 +8,7 @@ use thiserror::Error; use serde::{Deserialize, Serialize}; use url::Url; -use super::layout::{RunPlugin, RunPluginLocation}; +use super::layout::{PluginUserConfiguration, RunPlugin, RunPluginLocation}; #[cfg(not(target_family = "wasm"))] use crate::consts::ASSET_MAP; pub use crate::data::PluginTag; @@ -48,9 +48,11 @@ impl PluginsConfig { run: PluginType::Pane(None), _allow_exec_host_cmd: run._allow_exec_host_cmd, location: run.location.clone(), + userspace_configuration: run.configuration.clone(), }), RunPluginLocation::Zellij(tag) => self.0.get(tag).cloned().map(|plugin| PluginConfig { _allow_exec_host_cmd: run._allow_exec_host_cmd, + userspace_configuration: run.configuration.clone(), ..plugin }), } @@ -86,6 +88,8 @@ pub struct PluginConfig { pub _allow_exec_host_cmd: bool, /// Original location of the pub location: RunPluginLocation, + /// Custom configuration for this plugin + pub userspace_configuration: PluginUserConfiguration, } impl PluginConfig { diff --git a/zellij-utils/src/input/unit/layout_test.rs b/zellij-utils/src/input/unit/layout_test.rs index fee88aae44..038161a49a 100644 --- a/zellij-utils/src/input/unit/layout_test.rs +++ b/zellij-utils/src/input/unit/layout_test.rs @@ -504,9 +504,18 @@ fn layout_with_plugin_panes() { pane { plugin location="file:/path/to/my/plugin.wasm" } + pane { + plugin location="zellij:status-bar" { + config_key_1 "config_value_1" + "2" true + } + } } "#; let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None, None).unwrap(); + let mut expected_plugin_configuration = BTreeMap::new(); + expected_plugin_configuration.insert("config_key_1".to_owned(), "config_value_1".to_owned()); + expected_plugin_configuration.insert("2".to_owned(), "true".to_owned()); let expected_layout = Layout { template: Some(( TiledPaneLayout { @@ -515,6 +524,7 @@ fn layout_with_plugin_panes() { run: Some(Run::Plugin(RunPlugin { location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), _allow_exec_host_cmd: false, + configuration: Default::default(), })), ..Default::default() }, @@ -524,6 +534,15 @@ fn layout_with_plugin_panes() { "/path/to/my/plugin.wasm", )), _allow_exec_host_cmd: false, + configuration: Default::default(), + })), + ..Default::default() + }, + TiledPaneLayout { + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), + _allow_exec_host_cmd: false, + configuration: PluginUserConfiguration(expected_plugin_configuration), })), ..Default::default() }, @@ -2016,6 +2035,7 @@ fn run_plugin_location_parsing() { run: Some(Run::Plugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + configuration: Default::default(), })), ..Default::default() }, @@ -2025,6 +2045,7 @@ fn run_plugin_location_parsing() { location: RunPluginLocation::File(PathBuf::from( "/path/to/my/plugin.wasm", )), + configuration: Default::default(), })), ..Default::default() }, @@ -2032,6 +2053,7 @@ fn run_plugin_location_parsing() { run: Some(Run::Plugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from("plugin.wasm")), + configuration: Default::default(), })), ..Default::default() }, @@ -2041,6 +2063,7 @@ fn run_plugin_location_parsing() { location: RunPluginLocation::File(PathBuf::from( "relative/with space/plugin.wasm", )), + configuration: Default::default(), })), ..Default::default() }, @@ -2050,6 +2073,7 @@ fn run_plugin_location_parsing() { location: RunPluginLocation::File(PathBuf::from( "/absolute/with space/plugin.wasm", )), + configuration: Default::default(), })), ..Default::default() }, @@ -2059,6 +2083,7 @@ fn run_plugin_location_parsing() { location: RunPluginLocation::File(PathBuf::from( "c:/absolute/windows/plugin.wasm", )), + configuration: Default::default(), })), ..Default::default() }, diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap index 98d4fef5a1..e9f849b7f6 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1850 +assertion_line: 1863 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -66,6 +66,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -150,6 +153,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -194,6 +200,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -331,6 +340,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -375,6 +387,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -578,6 +593,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), diff --git a/zellij-utils/src/kdl/kdl_layout_parser.rs b/zellij-utils/src/kdl/kdl_layout_parser.rs index a5cab4b1bc..9cee47988e 100644 --- a/zellij-utils/src/kdl/kdl_layout_parser.rs +++ b/zellij-utils/src/kdl/kdl_layout_parser.rs @@ -2,9 +2,9 @@ use crate::input::{ command::RunCommand, config::ConfigError, layout::{ - FloatingPaneLayout, Layout, LayoutConstraint, PercentOrFixed, Run, RunPlugin, - RunPluginLocation, SplitDirection, SplitSize, SwapFloatingLayout, SwapTiledLayout, - TiledPaneLayout, + FloatingPaneLayout, Layout, LayoutConstraint, PercentOrFixed, PluginUserConfiguration, Run, + RunPlugin, RunPluginLocation, SplitDirection, SplitSize, SwapFloatingLayout, + SwapTiledLayout, TiledPaneLayout, }, }; @@ -14,7 +14,8 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::str::FromStr; use crate::{ - kdl_child_with_name, kdl_children_nodes, kdl_get_bool_property_or_child_value, + kdl_child_with_name, kdl_children_nodes, kdl_first_entry_as_bool, kdl_first_entry_as_i64, + kdl_first_entry_as_string, kdl_get_bool_property_or_child_value, kdl_get_bool_property_or_child_value_with_error, kdl_get_child, kdl_get_int_property_or_child_value, kdl_get_property_or_child, kdl_get_string_property_or_child_value, kdl_get_string_property_or_child_value_with_error, @@ -121,6 +122,11 @@ impl<'a> KdlLayoutParser<'a> { || property_name == "min_panes" || property_name == "exact_panes" } + pub fn is_a_reserved_plugin_property(property_name: &str) -> bool { + property_name == "location" + || property_name == "_allow_exec_host_cmd" + || property_name == "path" + } fn assert_legal_node_name(&self, name: &str, kdl_node: &KdlNode) -> Result<(), ConfigError> { if name.contains(char::is_whitespace) { Err(ConfigError::new_layout_kdl_error( @@ -305,11 +311,62 @@ impl<'a> KdlLayoutParser<'a> { url_node.span().len(), ) })?; + let configuration = KdlLayoutParser::parse_plugin_user_configuration(&plugin_block)?; Ok(Some(Run::Plugin(RunPlugin { _allow_exec_host_cmd, location, + configuration, }))) } + pub fn parse_plugin_user_configuration( + plugin_block: &KdlNode, + ) -> Result { + let mut configuration = BTreeMap::new(); + for user_configuration_entry in plugin_block.entries() { + let name = user_configuration_entry.name(); + let value = user_configuration_entry.value(); + if let Some(name) = name { + let name = name.to_string(); + if KdlLayoutParser::is_a_reserved_plugin_property(&name) { + continue; + } + configuration.insert(name, value.to_string()); + } + // we ignore "bare" (eg. `plugin i_am_a_bare_true_argument { arg_one 1; }`) entries + // to prevent diverging behaviour with the keybindings config + } + if let Some(user_config) = kdl_children_nodes!(plugin_block) { + for user_configuration_entry in user_config { + let config_entry_name = kdl_name!(user_configuration_entry); + if KdlLayoutParser::is_a_reserved_plugin_property(&config_entry_name) { + continue; + } + let config_entry_str_value = kdl_first_entry_as_string!(user_configuration_entry) + .map(|s| format!("{}", s.to_string())); + let config_entry_int_value = kdl_first_entry_as_i64!(user_configuration_entry) + .map(|s| format!("{}", s.to_string())); + let config_entry_bool_value = kdl_first_entry_as_bool!(user_configuration_entry) + .map(|s| format!("{}", s.to_string())); + let config_entry_children = user_configuration_entry + .children() + .map(|s| format!("{}", s.to_string().trim())); + let config_entry_value = config_entry_str_value + .or(config_entry_int_value) + .or(config_entry_bool_value) + .or(config_entry_children) + .ok_or(ConfigError::new_kdl_error( + format!( + "Failed to parse plugin block configuration: {:?}", + user_configuration_entry + ), + plugin_block.span().offset(), + plugin_block.span().len(), + ))?; + configuration.insert(config_entry_name.into(), config_entry_value); + } + } + Ok(PluginUserConfiguration::new(configuration)) + } fn parse_args(&self, pane_node: &KdlNode) -> Result>, ConfigError> { match kdl_get_child!(pane_node, "args") { Some(kdl_args) => { diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index b05a677de4..1402b5eff9 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -5,12 +5,12 @@ 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, RunPlugin, RunPluginLocation}; +use crate::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation}; use crate::input::options::{Clipboard, OnForceClose, Options}; use crate::input::plugins::{PluginConfig, PluginTag, PluginType, PluginsConfig}; use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig}; use kdl_layout_parser::KdlLayoutParser; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use strum::IntoEnumIterator; use miette::NamedSource; @@ -137,6 +137,17 @@ macro_rules! kdl_first_entry_as_i64 { }; } +#[macro_export] +macro_rules! kdl_first_entry_as_bool { + ( $node:expr ) => { + $node + .entries() + .iter() + .next() + .and_then(|i| i.value().as_bool()) + }; +} + #[macro_export] macro_rules! entry_count { ( $node:expr ) => {{ @@ -901,9 +912,11 @@ impl TryFrom<(&KdlNode, &Options)> for Action { .unwrap_or(false); let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); let location = RunPluginLocation::parse(&plugin_path, Some(current_dir))?; + let configuration = KdlLayoutParser::parse_plugin_user_configuration(&kdl_action)?; let run_plugin = RunPlugin { location, _allow_exec_host_cmd: false, + configuration, }; Ok(Action::LaunchOrFocusPlugin(run_plugin, should_float)) }, @@ -1272,15 +1285,12 @@ macro_rules! kdl_get_string_property_or_child_value_with_error { #[macro_export] macro_rules! kdl_get_property_or_child { ( $kdl_node:expr, $name:expr ) => { - $kdl_node - .get($name) - // .and_then(|e| e.value().as_string()) - .or_else(|| { - $kdl_node - .children() - .and_then(|c| c.get($name)) - .and_then(|c| c.get(0)) - }) + $kdl_node.get($name).or_else(|| { + $kdl_node + .children() + .and_then(|c| c.get($name)) + .and_then(|c| c.get(0)) + }) }; } @@ -1409,30 +1419,6 @@ impl Options { } } -impl RunPlugin { - pub fn from_kdl(kdl_node: &KdlNode, cwd: Option) -> Result { - let _allow_exec_host_cmd = - kdl_get_child_entry_bool_value!(kdl_node, "_allow_exec_host_cmd").unwrap_or(false); - let string_url = kdl_get_child_entry_string_value!(kdl_node, "location").ok_or( - ConfigError::new_kdl_error( - "Plugins must have a location".into(), - kdl_node.span().offset(), - kdl_node.span().len(), - ), - )?; - let location = RunPluginLocation::parse(string_url, cwd).map_err(|e| { - ConfigError::new_layout_kdl_error( - e.to_string(), - kdl_node.span().offset(), - kdl_node.span().len(), - ) - })?; - Ok(RunPlugin { - _allow_exec_host_cmd, - location, - }) - } -} impl Layout { pub fn from_kdl( raw_layout: &str, @@ -1720,6 +1706,9 @@ impl PluginsConfig { run: PluginType::Pane(None), location: RunPluginLocation::Zellij(plugin_tag.clone()), _allow_exec_host_cmd: allow_exec_host_cmd, + userspace_configuration: PluginUserConfiguration::new(BTreeMap::new()), // TODO: consider removing the whole + // "plugins" section in the config + // because it's not used??? }; plugins.insert(plugin_tag, plugin_config); } @@ -1807,3 +1796,51 @@ impl Themes { Ok(themes) } } + +pub fn parse_plugin_user_configuration( + plugin_block: &KdlNode, +) -> Result, ConfigError> { + let mut configuration = BTreeMap::new(); + for user_configuration_entry in plugin_block.entries() { + let name = user_configuration_entry.name(); + let value = user_configuration_entry.value(); + if let Some(name) = name { + let name = name.to_string(); + if KdlLayoutParser::is_a_reserved_plugin_property(&name) { + continue; + } + configuration.insert(name, value.to_string()); + } + } + if let Some(user_config) = kdl_children_nodes!(plugin_block) { + for user_configuration_entry in user_config { + let config_entry_name = kdl_name!(user_configuration_entry); + if KdlLayoutParser::is_a_reserved_plugin_property(&config_entry_name) { + continue; + } + let config_entry_str_value = kdl_first_entry_as_string!(user_configuration_entry) + .map(|s| format!("{}", s.to_string())); + let config_entry_int_value = kdl_first_entry_as_i64!(user_configuration_entry) + .map(|s| format!("{}", s.to_string())); + let config_entry_bool_value = kdl_first_entry_as_bool!(user_configuration_entry) + .map(|s| format!("{}", s.to_string())); + let config_entry_children = user_configuration_entry + .children() + .map(|s| format!("{}", s.to_string().trim())); + let config_entry_value = config_entry_str_value + .or(config_entry_int_value) + .or(config_entry_bool_value) + .or(config_entry_children) + .ok_or(ConfigError::new_kdl_error( + format!( + "Failed to parse plugin block configuration: {:?}", + user_configuration_entry + ), + plugin_block.span().offset(), + plugin_block.span().len(), + ))?; + configuration.insert(config_entry_name.into(), config_entry_value); + } + } + Ok(configuration) +} diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-2.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-2.snap index 2af2cb5672..e294f81732 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-2.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-2.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/setup.rs +assertion_line: 640 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -29,6 +30,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -70,6 +74,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -121,6 +128,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -205,6 +215,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -249,6 +262,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -386,6 +402,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -430,6 +449,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -633,6 +655,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -684,6 +709,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -752,6 +780,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -796,6 +827,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -933,6 +967,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -977,6 +1014,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -1180,6 +1220,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -1231,6 +1274,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -1315,6 +1361,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), 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 c9267c329f..23da443fc1 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/setup.rs +assertion_line: 639 expression: "format!(\"{:#?}\", config)" --- Config { @@ -3557,6 +3558,9 @@ Config { "compact-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "status-bar", @@ -3571,6 +3575,9 @@ Config { "status-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "strider", @@ -3585,6 +3592,9 @@ Config { "strider", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "tab-bar", @@ -3599,6 +3609,9 @@ Config { "tab-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, }, ui: UiConfig { 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 2c8b209561..e5f37d87a4 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/setup.rs +assertion_line: 697 expression: "format!(\"{:#?}\", config)" --- Config { @@ -3557,6 +3558,9 @@ Config { "compact-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "status-bar", @@ -3571,6 +3575,9 @@ Config { "status-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "strider", @@ -3585,6 +3592,9 @@ Config { "strider", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "tab-bar", @@ -3599,6 +3609,9 @@ Config { "tab-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, }, ui: UiConfig { diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap index 4cea161343..bb06be461b 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/setup.rs +assertion_line: 753 expression: "format!(\"{:#?}\", config)" --- Config { @@ -97,6 +98,9 @@ Config { "compact-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "status-bar", @@ -111,6 +115,9 @@ Config { "status-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "strider", @@ -125,6 +132,9 @@ Config { "strider", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "tab-bar", @@ -139,6 +149,9 @@ Config { "tab-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, }, ui: UiConfig { diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap index 5f55ff1d71..5540a57105 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/setup.rs +assertion_line: 725 expression: "format!(\"{:#?}\", config)" --- Config { @@ -3557,6 +3558,9 @@ Config { "compact-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "some-other-plugin", @@ -3571,6 +3575,9 @@ Config { "some-other-plugin", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "status-bar", @@ -3585,6 +3592,9 @@ Config { "status-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "strider", @@ -3599,6 +3609,9 @@ Config { "strider", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "tab-bar", @@ -3613,6 +3626,9 @@ Config { "tab-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, }, ui: UiConfig { 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 4534d49124..b885dafffc 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 @@ -1,5 +1,6 @@ --- source: zellij-utils/src/setup.rs +assertion_line: 739 expression: "format!(\"{:#?}\", config)" --- Config { @@ -3861,6 +3862,9 @@ Config { "compact-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "status-bar", @@ -3875,6 +3879,9 @@ Config { "status-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "strider", @@ -3889,6 +3896,9 @@ Config { "strider", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "tab-bar", @@ -3903,6 +3913,9 @@ Config { "tab-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, }, ui: UiConfig { 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 414b1d84b6..b371b61ff3 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/setup.rs +assertion_line: 711 expression: "format!(\"{:#?}\", config)" --- Config { @@ -3557,6 +3558,9 @@ Config { "compact-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "status-bar", @@ -3571,6 +3575,9 @@ Config { "status-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "strider", @@ -3585,6 +3592,9 @@ Config { "strider", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, PluginTag( "tab-bar", @@ -3599,6 +3609,9 @@ Config { "tab-bar", ), ), + userspace_configuration: PluginUserConfiguration( + {}, + ), }, }, ui: UiConfig { From d614f16810257717b8b7bb628919dfbb12d5a6a7 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 25 Jul 2023 10:05:19 +0200 Subject: [PATCH 019/128] docs(changelog): configurable plugins --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db02f9d626..7b851e897a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * fix(rendering): occasional glitches while resizing (https://github.com/zellij-org/zellij/pull/2621) * fix(rendering): colored paneframes in mirrored sessions (https://github.com/zellij-org/zellij/pull/2625) * fix(sessions): use custom lists of adjectives and nouns for generating session names (https://github.com/zellij-org/zellij/pull/2122) +* feat(plugins): make plugins configurable (https://github.com/zellij-org/zellij/pull/2646) ## [0.37.2] - 2023-06-20 * hotfix: include theme files into binary (https://github.com/zellij-org/zellij/pull/2566) From 3d9154a1c20f06700849239d7daf39b814601787 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Thu, 27 Jul 2023 17:55:21 +0200 Subject: [PATCH 020/128] fix(terminal): properly handle resizes in alternate screen (#2654) --- zellij-server/src/output/mod.rs | 6 +++ zellij-server/src/panes/grid.rs | 38 ++++++++----------- ...d_tests__alternate_screen_change_size.snap | 32 ++++++++++------ 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/zellij-server/src/output/mod.rs b/zellij-server/src/output/mod.rs index efcb74313b..a2d0419701 100644 --- a/zellij-server/src/output/mod.rs +++ b/zellij-server/src/output/mod.rs @@ -910,6 +910,12 @@ impl OutputBuffer { if row_width < viewport_width { let mut padding = vec![EMPTY_TERMINAL_CHARACTER; viewport_width - row_width]; terminal_characters.append(&mut padding); + } else if row_width > viewport_width { + let width_offset = row.excess_width_until(viewport_width); + let truncate_position = viewport_width.saturating_sub(width_offset); + if truncate_position < terminal_characters.len() { + terminal_characters.truncate(truncate_position); + } } terminal_characters } diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index cf006d42f6..3ae47327a6 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -760,9 +760,16 @@ impl Grid { if new_columns == 0 || new_rows == 0 { return; } + if self.alternate_screen_state.is_some() { + // in alternate screen we do nothing but log the new size, the program in the terminal + // is in control now... + self.height = new_rows; + self.width = new_columns; + return; + } self.selection.reset(); self.sixel_grid.character_cell_size_possibly_changed(); - if new_columns != self.width && self.alternate_screen_state.is_none() { + if new_columns != self.width { self.horizontal_tabstops = create_horizontal_tabstops(new_columns); let mut cursor_canonical_line_index = self.cursor_canonical_line_index(); let cursor_index_in_canonical_line = self.cursor_index_in_canonical_line(); @@ -917,14 +924,6 @@ impl Grid { }, } }; - } else if new_columns != self.width && self.alternate_screen_state.is_some() { - // in alternate screen just truncate exceeding width - for row in &mut self.viewport { - if row.width() >= new_columns { - let truncate_at = row.position_accounting_for_widechars(new_columns); - row.columns.truncate(truncate_at); - } - } } if new_rows != self.height { let current_viewport_row_count = self.viewport.len(); @@ -959,18 +958,13 @@ impl Grid { .saturating_sub(row_count_to_transfer); }; } - if self.alternate_screen_state.is_none() { - transfer_rows_from_viewport_to_lines_above( - &mut self.viewport, - &mut self.lines_above, - &mut self.sixel_grid, - row_count_to_transfer, - new_columns, - ); - } else { - // in alternate screen, no scroll buffer, so just remove lines - self.viewport.drain(0..row_count_to_transfer); - } + transfer_rows_from_viewport_to_lines_above( + &mut self.viewport, + &mut self.lines_above, + &mut self.sixel_grid, + row_count_to_transfer, + new_columns, + ); }, Ordering::Equal => {}, } @@ -1340,7 +1334,7 @@ impl Grid { if character_width == 0 { return; } - if self.cursor.x + character_width > self.width { + if self.cursor.x + character_width > self.width && self.alternate_screen_state.is_none() { if self.disable_linewrap { return; } diff --git a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__alternate_screen_change_size.snap b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__alternate_screen_change_size.snap index df16b28c34..4378ebd6be 100644 --- a/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__alternate_screen_change_size.snap +++ b/zellij-server/src/panes/unit/snapshots/zellij_server__panes__grid__grid_tests__alternate_screen_change_size.snap @@ -1,16 +1,26 @@ --- source: zellij-server/src/panes/./unit/grid_tests.rs +assertion_line: 2137 expression: "format!(\"{:?}\", grid)" - --- -00 (C): line12aaaa -01 (C): line13aaaa -02 (C): line14aaaa -03 (C): line15aaaa -04 (C): line16aaaa -05 (C): line17aaaa -06 (C): line18aaaa -07 (C): line19a🦀a -08 (C): line20a🦀 -09 (C): line21🦀🦀 +00 (C): line2aaaaaaaaaaaaaaa +01 (C): line3aaaaaaaaaaaaaaa +02 (C): line4aaaaaaaaaaaaaaa +03 (C): line5aaaaaaaaaaaaaaa +04 (C): line6aaaaaaaaaaaaaaa +05 (C): line7aaaaaaaaaaaaaaa +06 (C): line8aaaaaaaaaaaaaaa +07 (C): line9aaaaaaaaaaaaaaa +08 (C): line10aaaaaaaaaaaaaa +09 (C): line11aaaaaaaaaaaaaa +10 (C): line12aaaaaaaaaaaaaa +11 (C): line13aaaaaaaaaaaaaa +12 (C): line14aaaaaaaaaaaaaa +13 (C): line15aaaaaaaaaaaaaa +14 (C): line16aaaaaaaaaaaaaa +15 (C): line17aaaaaaaaaaaaaa +16 (C): line18aaaaaaaaaaaaaa +17 (C): line19a🦀aaaaaaaaaaa +18 (C): line20a🦀🦀aaaaaaaaa +19 (C): line21🦀🦀🦀🦀🦀🦀🦀 From 7c3394713f625542b7def4703d17cbfeb171286b Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Thu, 27 Jul 2023 17:56:49 +0200 Subject: [PATCH 021/128] docs(changelog): focus glitches --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b851e897a..92387f987b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * fix(rendering): colored paneframes in mirrored sessions (https://github.com/zellij-org/zellij/pull/2625) * fix(sessions): use custom lists of adjectives and nouns for generating session names (https://github.com/zellij-org/zellij/pull/2122) * feat(plugins): make plugins configurable (https://github.com/zellij-org/zellij/pull/2646) +* fix(terminal): occasional glitches while changing focus (https://github.com/zellij-org/zellij/pull/2654) ## [0.37.2] - 2023-06-20 * hotfix: include theme files into binary (https://github.com/zellij-org/zellij/pull/2566) From df407304a938d88df67e216e76dc42f9578e879b Mon Sep 17 00:00:00 2001 From: Nacho114 <17376073+Nacho114@users.noreply.github.com> Date: Fri, 28 Jul 2023 17:24:31 +0200 Subject: [PATCH 022/128] feat(plugins): utility functions to find active pane and tab (#2652) --- zellij-tile/src/shim.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index bba1468945..c12a615289 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -372,6 +372,31 @@ pub fn rename_tab>(tab_position: i32, new_name: S) { unsafe { host_rename_tab() }; } +// Utility Functions + +/// Returns the `TabInfo` corresponding to the currently active tab +fn get_focused_tab(tab_infos: &Vec) -> Option { + for tab_info in tab_infos { + if tab_info.active { + return Some(tab_info.clone()); + } + } + return None; +} + +/// Returns the `PaneInfo` corresponding to the currently active pane (ignoring plugins) +fn get_focused_pane(tab_position: usize, pane_manifest: &PaneManifest) -> Option { + let panes = pane_manifest.panes.get(&tab_position); + if let Some(panes) = panes { + for pane in panes { + if pane.is_focused & !pane.is_plugin { + return Some(pane.clone()); + } + } + } + None +} + // Internal Functions #[doc(hidden)] From 7d4ef5124e4356acebb2aaaa10cb8705e219c06a Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Fri, 28 Jul 2023 17:25:57 +0200 Subject: [PATCH 023/128] docs(changelog): plugin api utility functions --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92387f987b..fa957baaff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * fix(sessions): use custom lists of adjectives and nouns for generating session names (https://github.com/zellij-org/zellij/pull/2122) * feat(plugins): make plugins configurable (https://github.com/zellij-org/zellij/pull/2646) * fix(terminal): occasional glitches while changing focus (https://github.com/zellij-org/zellij/pull/2654) +* feat(plugins): add utility functions to get focused tab/pane (https://github.com/zellij-org/zellij/pull/2652) ## [0.37.2] - 2023-06-20 * hotfix: include theme files into binary (https://github.com/zellij-org/zellij/pull/2566) From 0fb40003e086fe3991e70ad3695e6a2201850277 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 2 Aug 2023 11:41:51 +0200 Subject: [PATCH 024/128] feat(ui): break pane to new tab and move panes between tabs (#2664) * prototype * some tests * break out floating pane * break out plugin panes * add keybind and fix some minor issues * remove cli * move pane to left/right tab * update ui * adjust ui * style(fmt): rustfmt * style(comment): remove commented code * update snapshots --- default-plugins/status-bar/src/second_line.rs | 7 +- ...ests__e2e__cases__mirrored_sessions-2.snap | 2 +- zellij-server/src/panes/floating_panes/mod.rs | 14 + zellij-server/src/panes/tiled_panes/mod.rs | 48 +- zellij-server/src/plugins/mod.rs | 1 + ...new_tabs_with_layout_plugin_command-2.snap | 5 +- ...__new_tabs_with_layout_plugin_command.snap | 5 +- zellij-server/src/pty.rs | 6 +- zellij-server/src/route.rs | 19 + zellij-server/src/screen.rs | 178 +++++- zellij-server/src/tab/layout_applier.rs | 98 ++-- zellij-server/src/tab/mod.rs | 32 +- zellij-server/src/unit/screen_tests.rs | 526 ++++++++++++++++-- ...an_break_floating_pane_to_a_new_tab-2.snap | 26 + ...an_break_floating_pane_to_a_new_tab-3.snap | 26 + ...an_break_floating_pane_to_a_new_tab-4.snap | 26 + ...an_break_floating_pane_to_a_new_tab-5.snap | 6 + ..._can_break_floating_pane_to_a_new_tab.snap | 26 + ...k_floating_plugin_pane_to_a_new_tab-2.snap | 26 + ...k_floating_plugin_pane_to_a_new_tab-3.snap | 26 + ...k_floating_plugin_pane_to_a_new_tab-4.snap | 26 + ...k_floating_plugin_pane_to_a_new_tab-5.snap | 26 + ...k_floating_plugin_pane_to_a_new_tab-6.snap | 26 + ...k_floating_plugin_pane_to_a_new_tab-7.snap | 26 + ...k_floating_plugin_pane_to_a_new_tab-8.snap | 6 + ...eak_floating_plugin_pane_to_a_new_tab.snap | 26 + ..._screen_can_break_pane_to_a_new_tab-2.snap | 26 + ..._screen_can_break_pane_to_a_new_tab-3.snap | 26 + ..._screen_can_break_pane_to_a_new_tab-4.snap | 26 + ..._screen_can_break_pane_to_a_new_tab-5.snap | 6 + ...s__screen_can_break_pane_to_a_new_tab.snap | 26 + ..._can_break_plugin_pane_to_a_new_tab-2.snap | 26 + ..._can_break_plugin_pane_to_a_new_tab-3.snap | 26 + ..._can_break_plugin_pane_to_a_new_tab-4.snap | 26 + ..._can_break_plugin_pane_to_a_new_tab-5.snap | 26 + ..._can_break_plugin_pane_to_a_new_tab-6.snap | 6 + ...en_can_break_plugin_pane_to_a_new_tab.snap | 26 + ...een_can_move_pane_to_a_new_tab_left-2.snap | 26 + ...een_can_move_pane_to_a_new_tab_left-3.snap | 26 + ...een_can_move_pane_to_a_new_tab_left-4.snap | 26 + ...een_can_move_pane_to_a_new_tab_left-5.snap | 6 + ...creen_can_move_pane_to_a_new_tab_left.snap | 26 + ...en_can_move_pane_to_a_new_tab_right-2.snap | 26 + ...en_can_move_pane_to_a_new_tab_right-3.snap | 26 + ...en_can_move_pane_to_a_new_tab_right-4.snap | 26 + ...en_can_move_pane_to_a_new_tab_right-5.snap | 6 + ...reen_can_move_pane_to_a_new_tab_right.snap | 26 + ...k_last_selectable_pane_to_a_new_tab-2.snap | 6 + ...eak_last_selectable_pane_to_a_new_tab.snap | 26 + ...end_cli_new_tab_action_default_params.snap | 4 + ...i_new_tab_action_with_name_and_layout.snap | 6 +- zellij-utils/assets/config/default.kdl | 3 + zellij-utils/src/errors.rs | 3 + zellij-utils/src/input/actions.rs | 3 + zellij-utils/src/input/layout.rs | 30 + ..._test__args_added_to_args_in_template.snap | 5 +- ..._test__args_override_args_in_template.snap | 5 +- ..._define_a_stack_with_an_expanded_pane.snap | 6 + ...define_stacked_children_for_pane_node.snap | 6 +- ...ne_stacked_children_for_pane_template.snap | 7 +- ...ad_swap_layouts_from_a_different_file.snap | 38 +- ...n_not_as_first_child_of_pane_template.snap | 14 +- ...en_not_as_first_child_of_tab_template.snap | 14 +- ...it_added_to_close_on_exit_in_template.snap | 5 +- ...t_overrides_close_on_exit_in_template.snap | 5 +- ..._and_pane_template_both_with_children.snap | 19 +- ...ut_test__cwd_added_to_cwd_in_template.snap | 5 +- ...ut_test__cwd_override_cwd_in_template.snap | 5 +- ...ayout__layout_test__env_var_expansion.snap | 12 +- ...epended_to_panes_with_and_without_cwd.snap | 6 +- ...ith_and_without_cwd_in_pane_templates.snap | 9 +- ...with_and_without_cwd_in_tab_templates.snap | 8 +- ...global_cwd_given_to_panes_without_cwd.snap | 5 +- ...al_cwd_passed_from_layout_constructor.snap | 5 +- ...r_overrides_global_cwd_in_layout_file.snap | 5 +- ...lobal_cwd_prepended_to_panes_with_cwd.snap | 5 +- ...th_tab_cwd_given_to_panes_without_cwd.snap | 6 +- ..._with_command_panes_and_close_on_exit.snap | 2 + ...ith_command_panes_and_start_suspended.snap | 2 + ...est__layout_with_default_tab_template.snap | 22 +- ...t_with_nested_branched_pane_templates.snap | 13 +- ...st__layout_with_nested_pane_templates.snap | 10 +- ...__layout_with_pane_excluded_from_sync.snap | 3 + ...yout_test__layout_with_pane_templates.snap | 24 +- ...t__layout_with_tab_and_pane_templates.snap | 9 +- ...__layout_with_tabs_and_floating_panes.snap | 8 + ...s_overriden_by_its_consumers_bare_cwd.snap | 4 +- ...verriden_by_its_consumers_command_cwd.snap | 4 +- ..._consumer_command_does_not_have_a_cwd.snap | 4 +- ...cwd_is_overriden_by_its_consumers_cwd.snap | 4 +- ...t_cwd_receives_its_consumers_bare_cwd.snap | 4 +- ...d_overriden_by_its_consumers_bare_cwd.snap | 4 +- ...ated_to_its_consumer_command_with_cwd.snap | 4 +- ...d_to_its_consumer_command_without_cwd.snap | 4 +- ..._bare_propagated_to_its_consumer_edit.snap | 3 + ...mmand_propagated_to_its_consumer_edit.snap | 3 + ...t__tab_cwd_given_to_panes_without_cwd.snap | 6 +- ...__tab_cwd_prepended_to_panes_with_cwd.snap | 6 +- zellij-utils/src/kdl/mod.rs | 3 + zellij-utils/src/pane_size.rs | 6 + ...i_arguments_override_layout_options-2.snap | 3 +- ...efault_config_with_no_cli_arguments-2.snap | 98 ++++ ..._default_config_with_no_cli_arguments.snap | 24 + ...out_env_vars_override_config_env_vars.snap | 24 + ...out_options_override_config_options-2.snap | 3 +- ...ayout_plugins_override_config_plugins.snap | 24 + ..._layout_themes_override_config_themes.snap | 24 + ..._ui_config_overrides_config_ui_config.snap | 24 + 108 files changed, 2200 insertions(+), 157 deletions(-) create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-2.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-3.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-4.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-5.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-2.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-3.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-4.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-5.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-6.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-7.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-8.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-2.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-3.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-4.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-5.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-2.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-3.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-4.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-5.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-6.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-2.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-3.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-4.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-5.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-2.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-3.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-4.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-5.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_cannot_break_last_selectable_pane_to_a_new_tab-2.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_cannot_break_last_selectable_pane_to_a_new_tab.snap diff --git a/default-plugins/status-bar/src/second_line.rs b/default-plugins/status-bar/src/second_line.rs index 015618f06f..e392317444 100644 --- a/default-plugins/status-bar/src/second_line.rs +++ b/default-plugins/status-bar/src/second_line.rs @@ -166,7 +166,12 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec)> { (s("Rename"), s("Rename"), action_key(&km, &[A::SwitchToMode(IM::RenameTab), A::TabNameInput(vec![0])])), (s("Sync"), s("Sync"), action_key(&km, &[A::ToggleActiveSyncTab, TO_NORMAL])), - (s("Toggle"), s("Toggle"), action_key(&km, &[A::ToggleTab])), + (s("Break pane to new tab"), s("Break"), action_key(&km, &[A::BreakPane, TO_NORMAL])), + (s("Break pane to next/prev tab"), s("Break next/prev"), + action_key_group(&km, &[ + &[A::BreakPaneLeft, TO_NORMAL], + &[A::BreakPaneRight, TO_NORMAL] + ])), (s("Select pane"), s("Select"), to_normal_key), ]} else if mi.mode == IM::Resize { vec![ (s("Increase/Decrease size"), s("Increase/Decrease"), diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap index 205ff40c73..fa5d345aa9 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions-2.snap @@ -26,4 +26,4 @@ expression: second_runner_snapshot │ ││ │ └──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SEARCH  SESSION  QUIT  - New / <←→> Change focus / Close / Rename / Sync / Toggle / Select pane + New / <←→> Move / Close / Rename / Sync / Break / <[]> Break next/prev / Select diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 98c6046bf6..2de5b74eb2 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -898,4 +898,18 @@ impl FloatingPanes { } pane_infos } + pub fn set_geom_for_pane_with_run(&mut self, run: Option, geom: PaneGeom) { + match self + .panes + .iter_mut() + .find(|(_, p)| p.invoked_with() == &run) + { + Some((_, pane)) => { + pane.set_geom(geom); + }, + None => { + log::error!("Failed to find pane with run: {:?}", run); + }, + } + } } diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index f0a6e8436a..9aca1af4ae 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -182,9 +182,13 @@ impl TiledPanes { let has_room_for_new_pane = pane_grid .find_room_for_new_pane(cursor_height_width_ratio) .is_some(); - has_room_for_new_pane || pane_grid.has_room_for_new_stacked_pane() + has_room_for_new_pane || pane_grid.has_room_for_new_stacked_pane() || self.panes.is_empty() } fn add_pane(&mut self, pane_id: PaneId, mut pane: Box, should_relayout: bool) { + if self.panes.is_empty() { + self.panes.insert(pane_id, pane); + return; + } let cursor_height_width_ratio = self.cursor_height_width_ratio(); let mut pane_grid = TiledPaneGrid::new( &mut self.panes, @@ -250,6 +254,19 @@ impl TiledPanes { }) .collect() } + pub fn non_selectable_pane_geoms_inside_viewport(&self) -> Vec { + self.panes + .values() + .filter_map(|p| { + let geom = p.position_and_size(); + if !p.selectable() && is_inside_viewport(&self.viewport.borrow(), p) { + Some(geom.into()) + } else { + None + } + }) + .collect() + } pub fn first_selectable_pane_id(&self) -> Option { self.panes .iter() @@ -587,8 +604,6 @@ impl TiledPanes { pub fn focused_pane_id(&self, client_id: ClientId) -> Option { self.active_panes.get(&client_id).copied() } - // FIXME: Really not a fan of allowing this... Someone with more energy - // than me should clean this up someday... #[allow(clippy::borrowed_box)] pub fn get_pane(&self, pane_id: PaneId) -> Option<&Box> { self.panes.get(&pane_id) @@ -735,6 +750,29 @@ impl TiledPanes { pub fn get_panes(&self) -> impl Iterator)> { self.panes.iter() } + pub fn set_geom_for_pane_with_run( + &mut self, + run: Option, + geom: PaneGeom, + borderless: bool, + ) { + match self + .panes + .iter_mut() + .find(|(_, p)| p.invoked_with() == &run) + { + Some((_, pane)) => { + pane.set_geom(geom); + pane.set_borderless(borderless); + if self.draw_pane_frames { + pane.set_content_offset(Offset::frame(1)); + } + }, + None => { + log::error!("Failed to find pane with run: {:?}", run); + }, + } + } pub fn resize(&mut self, new_screen_size: Size) { // this is blocked out to appease the borrow checker { @@ -1501,11 +1539,11 @@ impl TiledPanes { self.set_pane_frames(self.draw_pane_frames); // recalculate pane frames and update size closed_pane } else { - self.panes.remove(&pane_id); + let closed_pane = self.panes.remove(&pane_id); // this is a bit of a roundabout way to say: this is the last pane and so the tab // should be destroyed self.active_panes.clear(&mut self.panes); - None + closed_pane } } pub fn hold_pane( diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 7791880f27..f36bdc2d2e 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -228,6 +228,7 @@ pub(crate) fn plugin_thread_main( }; let mut extracted_floating_plugins: Vec> = floating_panes_layout .iter() + .filter(|f| !f.already_running) .map(|f| f.run.clone()) .collect(); extracted_run_instructions.append(&mut extracted_floating_plugins); diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap index e165dab102..43571f91f7 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command-2.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 521 +assertion_line: 735 expression: "format!(\"{:#?}\", second_new_tab_event)" --- Some( @@ -24,6 +24,7 @@ Some( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -37,6 +38,7 @@ Some( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -47,6 +49,7 @@ Some( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ), [], diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap index af83de3269..5e9a17cb31 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__new_tabs_with_layout_plugin_command.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 520 +assertion_line: 734 expression: "format!(\"{:#?}\", first_new_tab_event)" --- Some( @@ -24,6 +24,7 @@ Some( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -37,6 +38,7 @@ Some( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -47,6 +49,7 @@ Some( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ), [], diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index 84f282f8de..f571d4381d 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -605,8 +605,10 @@ impl Pty { default_shell.unwrap_or_else(|| self.get_default_terminal(cwd, None)); self.fill_cwd(&mut default_shell, client_id); let extracted_run_instructions = layout.extract_run_instructions(); - let extracted_floating_run_instructions = - floating_panes_layout.iter().map(|f| f.run.clone()); + let extracted_floating_run_instructions = floating_panes_layout + .iter() + .filter(|f| !f.already_running) + .map(|f| f.run.clone()); let mut new_pane_pids: Vec<(u32, bool, Option, Result)> = vec![]; // (terminal_id, // starts_held, // run_command, diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 5702bf86d2..3d1287a218 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -695,6 +695,25 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, + Action::BreakPane => { + senders + .send_to_screen(ScreenInstruction::BreakPane( + default_layout.clone(), + default_shell.clone(), + client_id, + )) + .with_context(err_context)?; + }, + Action::BreakPaneRight => { + senders + .send_to_screen(ScreenInstruction::BreakPaneRight(client_id)) + .with_context(err_context)?; + }, + Action::BreakPaneLeft => { + senders + .send_to_screen(ScreenInstruction::BreakPaneLeft(client_id)) + .with_context(err_context)?; + }, } Ok(should_break) } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index d07be93c26..a58f346394 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -14,13 +14,14 @@ use zellij_utils::pane_size::{PaneGeom, Size, SizeInPixels}; use zellij_utils::{ input::command::TerminalAction, input::layout::{ - FloatingPaneLayout, PluginUserConfiguration, Run, RunPlugin, RunPluginLocation, - SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, + FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginLocation, SwapFloatingLayout, + SwapTiledLayout, TiledPaneLayout, }, persistence, position::Position, }; +use crate::background_jobs::BackgroundJob; use crate::os_input_output::ResizeCache; use crate::panes::alacritty_functions::xparse_color; use crate::panes::terminal_character::AnsiCode; @@ -281,6 +282,9 @@ pub enum ScreenInstruction { FocusPaneWithId(PaneId, bool, ClientId), // bool is should_float RenamePane(PaneId, Vec), RenameTab(usize, Vec), + BreakPane(Box, Option, ClientId), + BreakPaneRight(ClientId), + BreakPaneLeft(ClientId), } impl From<&ScreenInstruction> for ScreenContext { @@ -449,6 +453,9 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId, ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane, ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab, + ScreenInstruction::BreakPane(..) => ScreenContext::BreakPane, + ScreenInstruction::BreakPaneRight(..) => ScreenContext::BreakPaneRight, + ScreenInstruction::BreakPaneLeft(..) => ScreenContext::BreakPaneLeft, } } } @@ -612,6 +619,7 @@ impl Screen { &mut self, source_tab_index: usize, destination_tab_index: usize, + update_mode_infos: bool, clients_to_move: Option>, ) -> Result<()> { let err_context = || { @@ -632,9 +640,11 @@ impl Screen { destination_tab .add_multiple_clients(client_mode_info_in_source_tab) .with_context(err_context)?; - destination_tab - .update_input_modes() - .with_context(err_context)?; + if update_mode_infos { + destination_tab + .update_input_modes() + .with_context(err_context)?; + } destination_tab.set_force_render(); destination_tab.visible(true).with_context(err_context)?; } @@ -660,6 +670,7 @@ impl Screen { &mut self, new_tab_pos: usize, should_change_pane_focus: Option, + update_mode_infos: bool, client_id: ClientId, ) -> Result<()> { let err_context = || { @@ -679,8 +690,13 @@ impl Screen { let current_tab_index = current_tab.index; let new_tab_index = new_tab.index; if self.session_is_mirrored { - self.move_clients_between_tabs(current_tab_index, new_tab_index, None) - .with_context(err_context)?; + self.move_clients_between_tabs( + current_tab_index, + new_tab_index, + update_mode_infos, + None, + ) + .with_context(err_context)?; let all_connected_clients: Vec = self.connected_clients.borrow().iter().copied().collect(); for client_id in all_connected_clients { @@ -699,6 +715,7 @@ impl Screen { self.move_clients_between_tabs( current_tab_index, new_tab_index, + update_mode_infos, Some(vec![client_id]), ) .with_context(err_context)?; @@ -739,7 +756,7 @@ impl Screen { fn switch_active_tab_name(&mut self, name: String, client_id: ClientId) -> Result { match self.tabs.values().find(|t| t.name == name) { Some(new_tab) => { - self.switch_active_tab(new_tab.position, None, client_id)?; + self.switch_active_tab(new_tab.position, None, true, client_id)?; Ok(true) }, None => Ok(false), @@ -750,6 +767,7 @@ impl Screen { pub fn switch_tab_next( &mut self, should_change_pane_focus: Option, + update_mode_infos: bool, client_id: ClientId, ) -> Result<()> { let err_context = || format!("failed to switch to next tab for client {client_id}"); @@ -768,6 +786,7 @@ impl Screen { return self.switch_active_tab( new_tab_pos, should_change_pane_focus, + update_mode_infos, client_id, ); }, @@ -781,6 +800,7 @@ impl Screen { pub fn switch_tab_prev( &mut self, should_change_pane_focus: Option, + update_mode_infos: bool, client_id: ClientId, ) -> Result<()> { let err_context = || format!("failed to switch to previous tab for client {client_id}"); @@ -804,6 +824,7 @@ impl Screen { return self.switch_active_tab( new_tab_pos, should_change_pane_focus, + update_mode_infos, client_id, ); }, @@ -814,7 +835,7 @@ impl Screen { } pub fn go_to_tab(&mut self, tab_index: usize, client_id: ClientId) -> Result<()> { - self.switch_active_tab(tab_index.saturating_sub(1), None, client_id) + self.switch_active_tab(tab_index.saturating_sub(1), None, true, client_id) } pub fn go_to_tab_name(&mut self, name: String, client_id: ClientId) -> Result { @@ -1392,6 +1413,7 @@ impl Screen { for tab in self.tabs.values_mut() { tab.change_mode_info(mode_info.clone(), client_id); tab.mark_active_pane_for_rerender(client_id); + tab.update_input_modes()?; } if let Some(os_input) = &mut self.bus.os_input { @@ -1436,7 +1458,7 @@ impl Screen { .move_focus_left(client_id) .and_then(|success| { if !success { - self.switch_tab_prev(Some(Direction::Left), client_id) + self.switch_tab_prev(Some(Direction::Left), true, client_id) .context("failed to move focus to previous tab") } else { Ok(()) @@ -1471,7 +1493,7 @@ impl Screen { .move_focus_right(client_id) .and_then(|success| { if !success { - self.switch_tab_next(Some(Direction::Right), client_id) + self.switch_tab_next(Some(Direction::Right), true, client_id) .context("failed to move focus to next tab") } else { Ok(()) @@ -1558,6 +1580,127 @@ impl Screen { }; Ok(()) } + pub fn break_pane( + &mut self, + default_shell: Option, + default_layout: Box, + client_id: ClientId, + ) -> Result<()> { + let err_context = || "failed break pane out of tab".to_string(); + let active_tab = self.get_active_tab_mut(client_id)?; + if active_tab.get_selectable_tiled_panes_count() > 1 + || active_tab.get_visible_selectable_floating_panes_count() > 0 + { + let active_pane_id = active_tab + .get_active_pane_id(client_id) + .with_context(err_context)?; + let pane_to_break_is_floating = active_tab.are_floating_panes_visible(); + let active_pane = active_tab + .close_pane(active_pane_id, false, Some(client_id)) + .with_context(err_context)?; + let active_pane_run_instruction = active_pane.invoked_with().clone(); + let tab_index = self.get_new_tab_index(); + let swap_layouts = ( + default_layout.swap_tiled_layouts.clone(), + default_layout.swap_floating_layouts.clone(), + ); + self.new_tab(tab_index, swap_layouts, None, client_id)?; + let tab = self.tabs.get_mut(&tab_index).with_context(err_context)?; + let (mut tiled_panes_layout, mut floating_panes_layout) = default_layout.new_tab(); + if pane_to_break_is_floating { + tab.show_floating_panes(); + tab.add_floating_pane(active_pane, active_pane_id, Some(client_id))?; + if let Some(mut already_running_layout) = floating_panes_layout + .iter_mut() + .find(|i| i.run == active_pane_run_instruction) + { + already_running_layout.already_running = true; + } + } else { + tab.add_tiled_pane(active_pane, active_pane_id, Some(client_id))?; + tiled_panes_layout.ignore_run_instruction(active_pane_run_instruction.clone()); + } + self.bus.senders.send_to_plugin(PluginInstruction::NewTab( + None, + default_shell, + Some(tiled_panes_layout), + floating_panes_layout, + tab_index, + client_id, + ))?; + } else { + let active_pane_id = active_tab + .get_active_pane_id(client_id) + .with_context(err_context)?; + self.bus + .senders + .send_to_background_jobs(BackgroundJob::DisplayPaneError( + vec![active_pane_id], + "Cannot break single pane out!".into(), + )) + .with_context(err_context)?; + self.unblock_input()?; + } + Ok(()) + } + pub fn break_pane_to_new_tab( + &mut self, + direction: Direction, + client_id: ClientId, + ) -> Result<()> { + let err_context = || "failed break pane out of tab".to_string(); + if self.tabs.len() > 1 { + let (active_pane_id, active_pane, pane_to_break_is_floating) = { + let active_tab = self.get_active_tab_mut(client_id)?; + let active_pane_id = active_tab + .get_active_pane_id(client_id) + .with_context(err_context)?; + let pane_to_break_is_floating = active_tab.are_floating_panes_visible(); + let active_pane = active_tab + .close_pane(active_pane_id, false, Some(client_id)) + .with_context(err_context)?; + (active_pane_id, active_pane, pane_to_break_is_floating) + }; + let update_mode_infos = false; + match direction { + Direction::Right | Direction::Down => { + self.switch_tab_next(None, update_mode_infos, client_id)?; + }, + Direction::Left | Direction::Up => { + self.switch_tab_prev(None, update_mode_infos, client_id)?; + }, + }; + let new_active_tab = self.get_active_tab_mut(client_id)?; + + if pane_to_break_is_floating { + new_active_tab.show_floating_panes(); + new_active_tab.add_floating_pane(active_pane, active_pane_id, Some(client_id))?; + } else { + new_active_tab.hide_floating_panes(); + new_active_tab.add_tiled_pane(active_pane, active_pane_id, Some(client_id))?; + } + + self.report_tab_state()?; + self.report_pane_state()?; + } else { + let active_pane_id = { + let active_tab = self.get_active_tab_mut(client_id)?; + active_tab + .get_active_pane_id(client_id) + .with_context(err_context)? + }; + self.bus + .senders + .send_to_background_jobs(BackgroundJob::DisplayPaneError( + vec![active_pane_id], + "No other tabs to add pane to!".into(), + )) + .with_context(err_context)?; + } + self.unblock_input()?; + self.render()?; + Ok(()) + } fn unblock_input(&self) -> Result<()> { self.bus @@ -2256,12 +2399,12 @@ pub(crate) fn screen_thread_main( screen.report_pane_state()?; }, ScreenInstruction::SwitchTabNext(client_id) => { - screen.switch_tab_next(None, client_id)?; + screen.switch_tab_next(None, true, client_id)?; screen.unblock_input()?; screen.render()?; }, ScreenInstruction::SwitchTabPrev(client_id) => { - screen.switch_tab_prev(None, client_id)?; + screen.switch_tab_prev(None, true, client_id)?; screen.unblock_input()?; screen.render()?; }, @@ -2837,6 +2980,15 @@ pub(crate) fn screen_thread_main( } screen.report_tab_state()?; }, + ScreenInstruction::BreakPane(default_layout, default_shell, client_id) => { + screen.break_pane(default_shell, default_layout, client_id)?; + }, + ScreenInstruction::BreakPaneRight(client_id) => { + screen.break_pane_to_new_tab(Direction::Right, client_id)?; + }, + ScreenInstruction::BreakPaneLeft(client_id) => { + screen.break_pane_to_new_tab(Direction::Left, client_id)?; + }, } } Ok(()) diff --git a/zellij-server/src/tab/layout_applier.rs b/zellij-server/src/tab/layout_applier.rs index 4797e8f37c..b93bb52fd9 100644 --- a/zellij-server/src/tab/layout_applier.rs +++ b/zellij-server/src/tab/layout_applier.rs @@ -180,6 +180,11 @@ impl<'a> LayoutApplier<'a> { } } pane_focuser.focus_tiled_pane(&mut self.tiled_panes); + LayoutApplier::offset_viewport( + self.viewport.clone(), + self.tiled_panes, + self.draw_pane_frames, + ); }, Err(e) => { Err::<(), _>(anyError::msg(e)) @@ -200,6 +205,7 @@ impl<'a> LayoutApplier<'a> { let free_space = self.total_space_for_tiled_panes(); match layout.position_panes_in_space(&free_space, None) { Ok(positions_in_layout) => { + let mut run_instructions_to_ignore = layout.run_instructions_to_ignore.clone(); let positions_and_size = positions_in_layout.iter(); let mut new_terminal_ids = new_terminal_ids.iter(); @@ -211,8 +217,17 @@ impl<'a> LayoutApplier<'a> { }; for (layout, position_and_size) in positions_and_size { - // A plugin pane - if let Some(Run::Plugin(run)) = layout.run.clone() { + if let Some(position) = run_instructions_to_ignore + .iter() + .position(|r| r == &layout.run) + { + let run = run_instructions_to_ignore.remove(position); + self.tiled_panes.set_geom_for_pane_with_run( + run, + *position_and_size, + layout.borderless, + ); + } else if let Some(Run::Plugin(run)) = layout.run.clone() { let pane_title = run.location.to_string(); let pid = new_plugin_ids .get_mut(&run.location) @@ -320,10 +335,15 @@ impl<'a> LayoutApplier<'a> { let mut new_floating_terminal_ids = new_floating_terminal_ids.iter(); for floating_pane_layout in floating_panes_layout { layout_has_floating_panes = true; - if let Some(Run::Plugin(run)) = floating_pane_layout.run.clone() { - let position_and_size = self - .floating_panes - .position_floating_pane_layout(&floating_pane_layout); + let position_and_size = self + .floating_panes + .position_floating_pane_layout(&floating_pane_layout); + if floating_pane_layout.already_running { + self.floating_panes.set_geom_for_pane_with_run( + floating_pane_layout.run.clone(), + position_and_size, + ); + } else if let Some(Run::Plugin(run)) = floating_pane_layout.run.clone() { let pane_title = run.location.to_string(); let pid = new_plugin_ids .get_mut(&run.location) @@ -363,9 +383,6 @@ impl<'a> LayoutApplier<'a> { focused_floating_pane = Some(PaneId::Plugin(pid)); } } else if let Some((pid, hold_for_command)) = new_floating_terminal_ids.next() { - let position_and_size = self - .floating_panes - .position_floating_pane_layout(&floating_pane_layout); let next_terminal_position = get_next_terminal_position(&self.tiled_panes, &self.floating_panes); let initial_title = match &floating_pane_layout.run { @@ -504,28 +521,43 @@ impl<'a> LayoutApplier<'a> { self.tiled_panes.resize(new_screen_size); Ok(()) } - fn offset_viewport(&mut self, position_and_size: &Viewport) { - let mut viewport = self.viewport.borrow_mut(); - if position_and_size.x == viewport.x - && position_and_size.x + position_and_size.cols == viewport.x + viewport.cols - { - if position_and_size.y == viewport.y { - viewport.y += position_and_size.rows; - viewport.rows -= position_and_size.rows; - } else if position_and_size.y + position_and_size.rows == viewport.y + viewport.rows { - viewport.rows -= position_and_size.rows; - } - } - if position_and_size.y == viewport.y - && position_and_size.y + position_and_size.rows == viewport.y + viewport.rows + pub fn offset_viewport( + viewport: Rc>, + tiled_panes: &mut TiledPanes, + draw_pane_frames: bool, + ) { + let boundary_geoms = tiled_panes.non_selectable_pane_geoms_inside_viewport(); { - if position_and_size.x == viewport.x { - viewport.x += position_and_size.cols; - viewport.cols -= position_and_size.cols; - } else if position_and_size.x + position_and_size.cols == viewport.x + viewport.cols { - viewport.cols -= position_and_size.cols; + // curly braces here is so that we free viewport immediately when we're done + let mut viewport = viewport.borrow_mut(); + for position_and_size in boundary_geoms { + if position_and_size.x == viewport.x + && position_and_size.x + position_and_size.cols == viewport.x + viewport.cols + { + if position_and_size.y == viewport.y { + viewport.y += position_and_size.rows; + viewport.rows -= position_and_size.rows; + } else if position_and_size.y + position_and_size.rows + == viewport.y + viewport.rows + { + viewport.rows -= position_and_size.rows; + } + } + if position_and_size.y == viewport.y + && position_and_size.y + position_and_size.rows == viewport.y + viewport.rows + { + if position_and_size.x == viewport.x { + viewport.x += position_and_size.cols; + viewport.cols -= position_and_size.cols; + } else if position_and_size.x + position_and_size.cols + == viewport.x + viewport.cols + { + viewport.cols -= position_and_size.cols; + } + } } } + tiled_panes.set_pane_frames(draw_pane_frames); } fn adjust_viewport(&mut self) -> Result<()> { // here we offset the viewport after applying a tiled panes layout @@ -541,11 +573,11 @@ impl<'a> LayoutApplier<'a> { *display_area }; self.resize_whole_tab(display_area).context(err_context)?; - let boundary_geoms = self.tiled_panes.borderless_pane_geoms(); - for geom in boundary_geoms { - self.offset_viewport(&geom) - } - self.tiled_panes.set_pane_frames(self.draw_pane_frames); + LayoutApplier::offset_viewport( + self.viewport.clone(), + self.tiled_panes, + self.draw_pane_frames, + ); Ok(()) } fn set_focused_tiled_pane(&mut self, focus_pane_id: Option, client_id: ClientId) { diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 27feb5034b..441ecd241e 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -1780,9 +1780,25 @@ impl Tab { pub(crate) fn get_tiled_panes(&self) -> impl Iterator)> { self.tiled_panes.get_panes() } + fn get_floating_panes(&self) -> impl Iterator)> { + self.floating_panes.get_panes() + } fn get_selectable_tiled_panes(&self) -> impl Iterator)> { self.get_tiled_panes().filter(|(_, p)| p.selectable()) } + fn get_selectable_floating_panes(&self) -> impl Iterator)> { + self.get_floating_panes().filter(|(_, p)| p.selectable()) + } + pub fn get_selectable_tiled_panes_count(&self) -> usize { + self.get_selectable_tiled_panes().count() + } + pub fn get_visible_selectable_floating_panes_count(&self) -> usize { + if self.are_floating_panes_visible() { + self.get_selectable_floating_panes().count() + } else { + 0 + } + } fn get_next_terminal_position(&self) -> usize { let tiled_panes_count = self .tiled_panes @@ -2142,6 +2158,13 @@ impl Tab { self.tiled_panes.move_clients_out_of_pane(id); } } + // we do this here because if there is a non-selectable pane on the edge, we consider it + // outside the viewport (a ui-pane, eg. the status-bar and tab-bar) and need to adjust for it + LayoutApplier::offset_viewport( + self.viewport.clone(), + &mut self.tiled_panes, + self.draw_pane_frames, + ); } pub fn close_pane( &mut self, @@ -3137,6 +3160,7 @@ impl Tab { pub fn set_pane_frames(&mut self, should_set_pane_frames: bool) { self.tiled_panes.set_pane_frames(should_set_pane_frames); + self.draw_pane_frames = should_set_pane_frames; self.should_clear_display_before_rendering = true; self.set_force_render(); } @@ -3278,13 +3302,13 @@ impl Tab { plugin_pane.progress_animation_offset(); } } - fn show_floating_panes(&mut self) { + pub fn show_floating_panes(&mut self) { // this function is to be preferred to directly invoking floating_panes.toggle_show_panes(true) self.floating_panes.toggle_show_panes(true); self.tiled_panes.unfocus_all_panes(); } - fn hide_floating_panes(&mut self) { + pub fn hide_floating_panes(&mut self) { // this function is to be preferred to directly invoking // floating_panes.toggle_show_panes(false) self.floating_panes.toggle_show_panes(false); @@ -3355,7 +3379,7 @@ impl Tab { } pane_info } - fn add_floating_pane( + pub fn add_floating_pane( &mut self, mut pane: Box, pane_id: PaneId, @@ -3380,7 +3404,7 @@ impl Tab { } Ok(()) } - fn add_tiled_pane( + pub fn add_tiled_pane( &mut self, mut pane: Box, pane_id: PaneId, diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 8a683edc0f..8d1debd353 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -15,7 +15,7 @@ use zellij_utils::errors::{prelude::*, ErrorContext}; use zellij_utils::input::actions::Action; use zellij_utils::input::command::{RunCommand, TerminalAction}; use zellij_utils::input::layout::{ - Layout, Run, RunPlugin, RunPluginLocation, SplitDirection, TiledPaneLayout, + FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginLocation, SplitDirection, TiledPaneLayout, }; use zellij_utils::input::options::Options; use zellij_utils::ipc::IpcReceiverWithContext; @@ -271,7 +271,11 @@ struct MockScreen { } impl MockScreen { - pub fn run(&mut self, initial_layout: Option) -> std::thread::JoinHandle<()> { + pub fn run( + &mut self, + initial_layout: Option, + initial_floating_panes_layout: Vec, + ) -> std::thread::JoinHandle<()> { let config_options = self.config_options.clone(); let client_attributes = self.client_attributes.clone(); let screen_bus = Bus::new( @@ -302,7 +306,9 @@ impl MockScreen { .unwrap(); let pane_layout = initial_layout.unwrap_or_default(); let pane_count = pane_layout.extract_run_instructions().len(); + let floating_pane_count = initial_floating_panes_layout.len(); let mut pane_ids = vec![]; + let mut floating_pane_ids = vec![]; let mut plugin_ids = HashMap::new(); plugin_ids.insert( RunPluginLocation::File(PathBuf::from("/path/to/fake/plugin")), @@ -311,6 +317,9 @@ impl MockScreen { for i in 0..pane_count { pane_ids.push((i as u32, None)); } + for i in 0..floating_pane_count { + floating_pane_ids.push((i as u32, None)); + } let default_shell = None; let tab_name = None; let tab_index = self.last_opened_tab_index.map(|l| l + 1).unwrap_or(0); @@ -318,16 +327,19 @@ impl MockScreen { None, default_shell, Some(pane_layout.clone()), - vec![], // floating_panes_layout + initial_floating_panes_layout.clone(), + // vec![], // floating_panes_layout tab_name, (vec![], vec![]), // swap layouts self.main_client_id, )); let _ = self.to_screen.send(ScreenInstruction::ApplyLayout( pane_layout, - vec![], // floating panes layout + initial_floating_panes_layout, + // vec![], // floating panes layout pane_ids, - vec![], // floating pane ids + floating_pane_ids, + // vec![], // floating pane ids plugin_ids, tab_index, self.main_client_id, @@ -547,7 +559,7 @@ pub fn switch_to_prev_tab() { new_tab(&mut screen, 1, 1); new_tab(&mut screen, 2, 2); - screen.switch_tab_prev(None, 1).expect("TEST"); + screen.switch_tab_prev(None, true, 1).expect("TEST"); assert_eq!( screen.get_active_tab(1).unwrap().position, @@ -566,8 +578,8 @@ pub fn switch_to_next_tab() { new_tab(&mut screen, 1, 1); new_tab(&mut screen, 2, 2); - screen.switch_tab_prev(None, 1).expect("TEST"); - screen.switch_tab_next(None, 1).expect("TEST"); + screen.switch_tab_prev(None, true, 1).expect("TEST"); + screen.switch_tab_next(None, true, 1).expect("TEST"); assert_eq!( screen.get_active_tab(1).unwrap().position, @@ -641,7 +653,7 @@ pub fn close_the_middle_tab() { new_tab(&mut screen, 1, 1); new_tab(&mut screen, 2, 2); new_tab(&mut screen, 3, 3); - screen.switch_tab_prev(None, 1).expect("TEST"); + screen.switch_tab_prev(None, true, 1).expect("TEST"); screen.close_tab(1).expect("TEST"); assert_eq!(screen.tabs.len(), 2, "Two tabs left"); @@ -663,7 +675,7 @@ fn move_focus_left_at_left_screen_edge_changes_tab() { new_tab(&mut screen, 1, 1); new_tab(&mut screen, 2, 2); new_tab(&mut screen, 3, 3); - screen.switch_tab_prev(None, 1).expect("TEST"); + screen.switch_tab_prev(None, true, 1).expect("TEST"); screen.move_focus_left_or_previous_tab(1).expect("TEST"); assert_eq!( @@ -684,7 +696,7 @@ fn move_focus_right_at_right_screen_edge_changes_tab() { new_tab(&mut screen, 1, 1); new_tab(&mut screen, 2, 2); new_tab(&mut screen, 3, 3); - screen.switch_tab_prev(None, 1).expect("TEST"); + screen.switch_tab_prev(None, true, 1).expect("TEST"); screen.move_focus_right_or_next_tab(1).expect("TEST"); assert_eq!( @@ -820,7 +832,7 @@ pub fn toggle_to_previous_tab_delete() { "Active tab toggler to previous tab" ); - screen.switch_tab_prev(None, 1).expect("TEST"); + screen.switch_tab_prev(None, true, 1).expect("TEST"); assert_eq!( screen.tab_history.get(&1).unwrap(), &[0, 1, 3], @@ -831,7 +843,7 @@ pub fn toggle_to_previous_tab_delete() { 2, "Active tab toggler to previous tab" ); - screen.switch_tab_prev(None, 1).expect("TEST"); + screen.switch_tab_prev(None, true, 1).expect("TEST"); assert_eq!( screen.tab_history.get(&1).unwrap(), &[0, 3, 2], @@ -886,7 +898,7 @@ fn switch_to_tab_with_fullscreen() { } new_tab(&mut screen, 2, 2); - screen.switch_tab_prev(None, 1).expect("TEST"); + screen.switch_tab_prev(None, true, 1).expect("TEST"); assert_eq!( screen.get_active_tab(1).unwrap().position, @@ -1023,7 +1035,7 @@ pub fn send_cli_write_chars_action_to_screen() { let mut mock_screen = MockScreen::new(size); let pty_writer_receiver = mock_screen.pty_writer_receiver.take().unwrap(); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(None); + let screen_thread = mock_screen.run(None, vec![]); let received_pty_instructions = Arc::new(Mutex::new(vec![])); let pty_writer_thread = log_actions_in_thread!( received_pty_instructions, @@ -1049,7 +1061,7 @@ pub fn send_cli_write_action_to_screen() { let mut mock_screen = MockScreen::new(size); let pty_writer_receiver = mock_screen.pty_writer_receiver.take().unwrap(); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(None); + let screen_thread = mock_screen.run(None, vec![]); let received_pty_instructions = Arc::new(Mutex::new(vec![])); let pty_writer_thread = log_actions_in_thread!( received_pty_instructions, @@ -1074,7 +1086,7 @@ pub fn send_cli_resize_action_to_screen() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let pty_writer_thread = log_actions_in_thread!( @@ -1108,7 +1120,7 @@ pub fn send_cli_focus_next_pane_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1141,7 +1153,7 @@ pub fn send_cli_focus_previous_pane_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1174,7 +1186,7 @@ pub fn send_cli_move_focus_pane_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1209,7 +1221,7 @@ pub fn send_cli_move_focus_or_tab_pane_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1244,7 +1256,7 @@ pub fn send_cli_move_pane_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1278,7 +1290,7 @@ pub fn send_cli_dump_screen_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_thread = log_actions_in_thread!( @@ -1312,7 +1324,7 @@ pub fn send_cli_edit_scrollback_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_pty_instructions = Arc::new(Mutex::new(vec![])); let pty_receiver = mock_screen.pty_receiver.take().unwrap(); let pty_thread = log_actions_in_thread!( @@ -1360,7 +1372,7 @@ pub fn send_cli_scroll_up_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1404,7 +1416,7 @@ pub fn send_cli_scroll_down_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1454,7 +1466,7 @@ pub fn send_cli_scroll_to_bottom_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1507,7 +1519,7 @@ pub fn send_cli_scroll_to_top_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1549,7 +1561,7 @@ pub fn send_cli_page_scroll_up_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1590,7 +1602,7 @@ pub fn send_cli_page_scroll_down_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1638,7 +1650,7 @@ pub fn send_cli_half_page_scroll_up_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1679,7 +1691,7 @@ pub fn send_cli_half_page_scroll_down_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1735,7 +1747,7 @@ pub fn send_cli_toggle_full_screen_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1767,7 +1779,7 @@ pub fn send_cli_toggle_pane_frames_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -1803,7 +1815,7 @@ pub fn send_cli_toggle_active_tab_sync_action() { let mut initial_layout = TiledPaneLayout::default(); initial_layout.children_split_direction = SplitDirection::Vertical; initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_pty_instructions = Arc::new(Mutex::new(vec![])); let pty_writer_thread = log_actions_in_thread!( received_pty_instructions, @@ -1838,7 +1850,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() { let mut initial_layout = TiledPaneLayout::default(); initial_layout.children_split_direction = SplitDirection::Vertical; initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_pty_instructions = Arc::new(Mutex::new(vec![])); let pty_thread = log_actions_in_thread!( received_pty_instructions, @@ -1875,7 +1887,7 @@ pub fn send_cli_new_pane_action_with_split_direction() { let mut initial_layout = TiledPaneLayout::default(); initial_layout.children_split_direction = SplitDirection::Vertical; initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_pty_instructions = Arc::new(Mutex::new(vec![])); let pty_thread = log_actions_in_thread!( received_pty_instructions, @@ -1912,7 +1924,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() { let mut initial_layout = TiledPaneLayout::default(); initial_layout.children_split_direction = SplitDirection::Vertical; initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_pty_instructions = Arc::new(Mutex::new(vec![])); let pty_thread = log_actions_in_thread!( received_pty_instructions, @@ -1949,7 +1961,7 @@ pub fn send_cli_edit_action_with_default_parameters() { let mut initial_layout = TiledPaneLayout::default(); initial_layout.children_split_direction = SplitDirection::Vertical; initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_pty_instructions = Arc::new(Mutex::new(vec![])); let pty_thread = log_actions_in_thread!( received_pty_instructions, @@ -1982,7 +1994,7 @@ pub fn send_cli_edit_action_with_line_number() { let mut initial_layout = TiledPaneLayout::default(); initial_layout.children_split_direction = SplitDirection::Vertical; initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_pty_instructions = Arc::new(Mutex::new(vec![])); let pty_thread = log_actions_in_thread!( received_pty_instructions, @@ -2015,7 +2027,7 @@ pub fn send_cli_edit_action_with_split_direction() { let mut initial_layout = TiledPaneLayout::default(); initial_layout.children_split_direction = SplitDirection::Vertical; initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_pty_instructions = Arc::new(Mutex::new(vec![])); let pty_thread = log_actions_in_thread!( received_pty_instructions, @@ -2047,7 +2059,7 @@ pub fn send_cli_switch_mode_action() { let mut initial_layout = TiledPaneLayout::default(); initial_layout.children_split_direction = SplitDirection::Vertical; initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let cli_switch_mode = CliAction::SwitchMode { input_mode: InputMode::Locked, }; @@ -2073,7 +2085,7 @@ pub fn send_cli_toggle_pane_embed_or_float() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -2117,7 +2129,7 @@ pub fn send_cli_toggle_floating_panes() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -2157,7 +2169,7 @@ pub fn send_cli_close_pane_action() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_instruction = log_actions_in_thread!( @@ -2189,7 +2201,7 @@ pub fn send_cli_new_tab_action_default_params() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_plugin_instructions = Arc::new(Mutex::new(vec![])); let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); let plugin_thread = log_actions_in_thread!( @@ -2226,7 +2238,7 @@ pub fn send_cli_new_tab_action_with_name_and_layout() { initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_plugin_instructions = Arc::new(Mutex::new(vec![])); let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); let plugin_thread = log_actions_in_thread!( @@ -2276,7 +2288,7 @@ pub fn send_cli_next_tab_action() { let mut mock_screen = MockScreen::new(size); mock_screen.new_tab(second_tab_layout); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_thread = log_actions_in_thread!( @@ -2312,7 +2324,7 @@ pub fn send_cli_previous_tab_action() { let mut mock_screen = MockScreen::new(size); mock_screen.new_tab(second_tab_layout); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_thread = log_actions_in_thread!( @@ -2348,7 +2360,7 @@ pub fn send_cli_goto_tab_action() { let mut mock_screen = MockScreen::new(size); mock_screen.new_tab(second_tab_layout); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_thread = log_actions_in_thread!( @@ -2384,7 +2396,7 @@ pub fn send_cli_close_tab_action() { let mut mock_screen = MockScreen::new(size); mock_screen.new_tab(second_tab_layout); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_thread = log_actions_in_thread!( @@ -2420,7 +2432,7 @@ pub fn send_cli_rename_tab() { let mut mock_screen = MockScreen::new(size); mock_screen.new_tab(second_tab_layout); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_plugin_instructions = Arc::new(Mutex::new(vec![])); let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); let plugin_thread = log_actions_in_thread!( @@ -2465,7 +2477,7 @@ pub fn send_cli_undo_rename_tab() { let mut mock_screen = MockScreen::new(size); mock_screen.new_tab(second_tab_layout); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_plugin_instructions = Arc::new(Mutex::new(vec![])); let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); let plugin_thread = log_actions_in_thread!( @@ -2509,7 +2521,7 @@ pub fn send_cli_query_tab_names_action() { let mut mock_screen = MockScreen::new(size); mock_screen.new_tab(TiledPaneLayout::default()); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(Some(TiledPaneLayout::default())); + let screen_thread = mock_screen.run(Some(TiledPaneLayout::default()), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_thread = log_actions_in_thread!( @@ -2543,7 +2555,7 @@ pub fn send_cli_launch_or_focus_plugin_action() { let mut mock_screen = MockScreen::new(size); let plugin_receiver = mock_screen.plugin_receiver.take().unwrap(); let session_metadata = mock_screen.clone_session_metadata(); - let screen_thread = mock_screen.run(None); + let screen_thread = mock_screen.run(None, vec![]); let received_plugin_instructions = Arc::new(Mutex::new(vec![])); let plugin_thread = log_actions_in_thread!( received_plugin_instructions, @@ -2593,7 +2605,7 @@ pub fn send_cli_launch_or_focus_plugin_action_when_plugin_is_already_loaded() { }; initial_layout.children_split_direction = SplitDirection::Vertical; initial_layout.children = vec![TiledPaneLayout::default(), existing_plugin_pane]; - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_plugin_instructions = Arc::new(Mutex::new(vec![])); let plugin_thread = log_actions_in_thread!( received_plugin_instructions, @@ -2653,7 +2665,7 @@ pub fn screen_can_suppress_pane() { initial_layout.children_split_direction = SplitDirection::Vertical; initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()]; let mut mock_screen = MockScreen::new(size); - let screen_thread = mock_screen.run(Some(initial_layout)); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); let received_server_instructions = Arc::new(Mutex::new(vec![])); let server_receiver = mock_screen.server_receiver.take().unwrap(); let server_thread = log_actions_in_thread!( @@ -2677,3 +2689,403 @@ pub fn screen_can_suppress_pane() { } assert_snapshot!(format!("{}", snapshot_count)); } + +#[test] +pub fn screen_can_break_pane_to_a_new_tab() { + let size = Size { cols: 80, rows: 20 }; + let mut initial_layout = TiledPaneLayout::default(); + let mut pane_to_break_free = TiledPaneLayout::default(); + pane_to_break_free.name = Some("pane_to_break_free".to_owned()); + let mut pane_to_stay = TiledPaneLayout::default(); + pane_to_stay.name = Some("pane_to_stay".to_owned()); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![pane_to_break_free, pane_to_stay]; + let mut mock_screen = MockScreen::new(size); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + + let _ = mock_screen.to_screen.send(ScreenInstruction::BreakPane( + Box::new(Layout::default()), + Default::default(), + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + // we send ApplyLayout, because in prod this is eventually received after the message traverses + // through the plugin and pty threads (to open extra stuff we need in the layout, eg. the + // default plugins) + let _ = mock_screen.to_screen.send(ScreenInstruction::ApplyLayout( + TiledPaneLayout::default(), + vec![], // floating_panes_layout + Default::default(), + vec![], // floating panes ids + Default::default(), + 1, + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + // move back to make sure the other pane is in the previous tab + let _ = mock_screen + .to_screen + .send(ScreenInstruction::MoveFocusLeftOrPreviousTab(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + // move forward to make sure the broken pane is in the previous tab + let _ = mock_screen + .to_screen + .send(ScreenInstruction::MoveFocusRightOrNextTab(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + + mock_screen.teardown(vec![server_thread, screen_thread]); + + let snapshots = take_snapshots_and_cursor_coordinates_from_render_events( + received_server_instructions.lock().unwrap().iter(), + size, + ); + let snapshot_count = snapshots.len(); + for (_cursor_coordinates, snapshot) in snapshots { + assert_snapshot!(format!("{}", snapshot)); + } + assert_snapshot!(format!("{}", snapshot_count)); +} + +#[test] +pub fn screen_cannot_break_last_selectable_pane_to_a_new_tab() { + let size = Size { cols: 80, rows: 20 }; + let initial_layout = TiledPaneLayout::default(); + let mut mock_screen = MockScreen::new(size); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + + let _ = mock_screen.to_screen.send(ScreenInstruction::BreakPane( + Box::new(Layout::default()), + Default::default(), + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + + mock_screen.teardown(vec![server_thread, screen_thread]); + + let snapshots = take_snapshots_and_cursor_coordinates_from_render_events( + received_server_instructions.lock().unwrap().iter(), + size, + ); + let snapshot_count = snapshots.len(); + for (_cursor_coordinates, snapshot) in snapshots { + assert_snapshot!(format!("{}", snapshot)); + } + assert_snapshot!(format!("{}", snapshot_count)); +} + +#[test] +pub fn screen_can_break_floating_pane_to_a_new_tab() { + let size = Size { cols: 80, rows: 20 }; + let mut initial_layout = TiledPaneLayout::default(); + let mut pane_to_break_free = TiledPaneLayout::default(); + pane_to_break_free.name = Some("tiled_pane".to_owned()); + let mut floating_pane = FloatingPaneLayout::default(); + floating_pane.name = Some("floating_pane_to_eject".to_owned()); + let mut floating_panes_layout = vec![floating_pane]; + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![pane_to_break_free]; + let mut mock_screen = MockScreen::new(size); + let screen_thread = mock_screen.run(Some(initial_layout), floating_panes_layout.clone()); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + + let _ = mock_screen.to_screen.send(ScreenInstruction::BreakPane( + Box::new(Layout::default()), + Default::default(), + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + // we send ApplyLayout, because in prod this is eventually received after the message traverses + // through the plugin and pty threads (to open extra stuff we need in the layout, eg. the + // default plugins) + floating_panes_layout.get_mut(0).unwrap().already_running = true; + let _ = mock_screen.to_screen.send(ScreenInstruction::ApplyLayout( + TiledPaneLayout::default(), + floating_panes_layout, + vec![(1, None)], // tiled pane ids - send these because one needs to be created under the + // ejected floating pane, lest the tab be closed as having no tiled panes + // (this happens in prod in the pty thread) + vec![], // floating panes ids + Default::default(), + 1, + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + // move back to make sure the other pane is in the previous tab + let _ = mock_screen + .to_screen + .send(ScreenInstruction::MoveFocusLeftOrPreviousTab(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + // move forward to make sure the broken pane is in the previous tab + let _ = mock_screen + .to_screen + .send(ScreenInstruction::MoveFocusRightOrNextTab(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + + mock_screen.teardown(vec![server_thread, screen_thread]); + + let snapshots = take_snapshots_and_cursor_coordinates_from_render_events( + received_server_instructions.lock().unwrap().iter(), + size, + ); + let snapshot_count = snapshots.len(); + for (_cursor_coordinates, snapshot) in snapshots { + assert_snapshot!(format!("{}", snapshot)); + } + assert_snapshot!(format!("{}", snapshot_count)); +} + +#[test] +pub fn screen_can_break_plugin_pane_to_a_new_tab() { + let size = Size { cols: 80, rows: 20 }; + let mut initial_layout = TiledPaneLayout::default(); + let mut pane_to_break_free = TiledPaneLayout::default(); + pane_to_break_free.name = Some("plugin_pane_to_break_free".to_owned()); + pane_to_break_free.run = Some(Run::Plugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from("/path/to/fake/plugin")), + configuration: Default::default(), + })); + let mut pane_to_stay = TiledPaneLayout::default(); + pane_to_stay.name = Some("pane_to_stay".to_owned()); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![pane_to_break_free, pane_to_stay]; + let mut mock_screen = MockScreen::new(size); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + + let _ = mock_screen.to_screen.send(ScreenInstruction::BreakPane( + Box::new(Layout::default()), + Default::default(), + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + // we send ApplyLayout, because in prod this is eventually received after the message traverses + // through the plugin and pty threads (to open extra stuff we need in the layout, eg. the + // default plugins) + let _ = mock_screen.to_screen.send(ScreenInstruction::ApplyLayout( + TiledPaneLayout::default(), + vec![], // floating_panes_layout + Default::default(), + vec![], // floating panes ids + Default::default(), + 1, + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + // move back to make sure the other pane is in the previous tab + let _ = mock_screen + .to_screen + .send(ScreenInstruction::MoveFocusLeftOrPreviousTab(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + // move forward to make sure the broken pane is in the previous tab + let _ = mock_screen + .to_screen + .send(ScreenInstruction::MoveFocusRightOrNextTab(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + + mock_screen.teardown(vec![server_thread, screen_thread]); + + let snapshots = take_snapshots_and_cursor_coordinates_from_render_events( + received_server_instructions.lock().unwrap().iter(), + size, + ); + let snapshot_count = snapshots.len(); + for (_cursor_coordinates, snapshot) in snapshots { + assert_snapshot!(format!("{}", snapshot)); + } + assert_snapshot!(format!("{}", snapshot_count)); +} + +#[test] +pub fn screen_can_break_floating_plugin_pane_to_a_new_tab() { + let size = Size { cols: 80, rows: 20 }; + let mut initial_layout = TiledPaneLayout::default(); + let mut pane_to_break_free = TiledPaneLayout::default(); + pane_to_break_free.name = Some("tiled_pane".to_owned()); + let mut floating_pane = FloatingPaneLayout::default(); + floating_pane.name = Some("floating_plugin_pane_to_eject".to_owned()); + floating_pane.run = Some(Run::Plugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from("/path/to/fake/plugin")), + configuration: Default::default(), + })); + let mut floating_panes_layout = vec![floating_pane]; + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![pane_to_break_free]; + let mut mock_screen = MockScreen::new(size); + let screen_thread = mock_screen.run(Some(initial_layout), floating_panes_layout.clone()); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + + let _ = mock_screen.to_screen.send(ScreenInstruction::BreakPane( + Box::new(Layout::default()), + Default::default(), + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + // we send ApplyLayout, because in prod this is eventually received after the message traverses + // through the plugin and pty threads (to open extra stuff we need in the layout, eg. the + // default plugins) + floating_panes_layout.get_mut(0).unwrap().already_running = true; + let _ = mock_screen.to_screen.send(ScreenInstruction::ApplyLayout( + TiledPaneLayout::default(), + floating_panes_layout, + vec![(1, None)], // tiled pane ids - send these because one needs to be created under the + // ejected floating pane, lest the tab be closed as having no tiled panes + // (this happens in prod in the pty thread) + vec![], // floating panes ids + Default::default(), + 1, + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + // move back to make sure the other pane is in the previous tab + let _ = mock_screen + .to_screen + .send(ScreenInstruction::MoveFocusLeftOrPreviousTab(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + // move forward to make sure the broken pane is in the previous tab + let _ = mock_screen + .to_screen + .send(ScreenInstruction::MoveFocusRightOrNextTab(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + + mock_screen.teardown(vec![server_thread, screen_thread]); + + let snapshots = take_snapshots_and_cursor_coordinates_from_render_events( + received_server_instructions.lock().unwrap().iter(), + size, + ); + let snapshot_count = snapshots.len(); + for (_cursor_coordinates, snapshot) in snapshots { + assert_snapshot!(format!("{}", snapshot)); + } + assert_snapshot!(format!("{}", snapshot_count)); +} + +#[test] +pub fn screen_can_move_pane_to_a_new_tab_right() { + let size = Size { cols: 80, rows: 20 }; + let mut initial_layout = TiledPaneLayout::default(); + let mut pane_to_break_free = TiledPaneLayout::default(); + pane_to_break_free.name = Some("pane_to_break_free".to_owned()); + let mut pane_to_stay = TiledPaneLayout::default(); + pane_to_stay.name = Some("pane_to_stay".to_owned()); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![pane_to_break_free, pane_to_stay]; + let mut mock_screen = MockScreen::new(size); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + + let _ = mock_screen.to_screen.send(ScreenInstruction::BreakPane( + Box::new(Layout::default()), + Default::default(), + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + let _ = mock_screen + .to_screen + .send(ScreenInstruction::MoveFocusLeftOrPreviousTab(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + let _ = mock_screen + .to_screen + .send(ScreenInstruction::BreakPaneRight(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + + mock_screen.teardown(vec![server_thread, screen_thread]); + + let snapshots = take_snapshots_and_cursor_coordinates_from_render_events( + received_server_instructions.lock().unwrap().iter(), + size, + ); + let snapshot_count = snapshots.len(); + for (_cursor_coordinates, snapshot) in snapshots { + assert_snapshot!(format!("{}", snapshot)); + } + assert_snapshot!(format!("{}", snapshot_count)); +} + +#[test] +pub fn screen_can_move_pane_to_a_new_tab_left() { + let size = Size { cols: 80, rows: 20 }; + let mut initial_layout = TiledPaneLayout::default(); + let mut pane_to_break_free = TiledPaneLayout::default(); + pane_to_break_free.name = Some("pane_to_break_free".to_owned()); + let mut pane_to_stay = TiledPaneLayout::default(); + pane_to_stay.name = Some("pane_to_stay".to_owned()); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![pane_to_break_free, pane_to_stay]; + let mut mock_screen = MockScreen::new(size); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + + let _ = mock_screen.to_screen.send(ScreenInstruction::BreakPane( + Box::new(Layout::default()), + Default::default(), + 1, + )); + std::thread::sleep(std::time::Duration::from_millis(100)); + let _ = mock_screen + .to_screen + .send(ScreenInstruction::MoveFocusLeftOrPreviousTab(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + let _ = mock_screen + .to_screen + .send(ScreenInstruction::BreakPaneLeft(1)); + std::thread::sleep(std::time::Duration::from_millis(100)); + + mock_screen.teardown(vec![server_thread, screen_thread]); + + let snapshots = take_snapshots_and_cursor_coordinates_from_render_events( + received_server_instructions.lock().unwrap().iter(), + size, + ); + let snapshot_count = snapshots.len(); + for (_cursor_coordinates, snapshot) in snapshots { + assert_snapshot!(format!("{}", snapshot)); + } + assert_snapshot!(format!("{}", snapshot_count)); +} diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-2.snap new file mode 100644 index 0000000000..5fa4bb26c5 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-2.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2849 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #2 ─────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ ┌ floating_pane_to_eject ──────────────┐ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ │ │ │ +16 (C): │ └──────────────────────────────────────┘ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-3.snap new file mode 100644 index 0000000000..6274f758c5 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-3.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2849 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ tiled_pane ──────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-4.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-4.snap new file mode 100644 index 0000000000..5fa4bb26c5 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-4.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2849 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #2 ─────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ ┌ floating_pane_to_eject ──────────────┐ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ │ │ │ +16 (C): │ └──────────────────────────────────────┘ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-5.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-5.snap new file mode 100644 index 0000000000..caa8e8714c --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab-5.snap @@ -0,0 +1,6 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2851 +expression: "format!(\"{}\", snapshot_count)" +--- +4 diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab.snap new file mode 100644 index 0000000000..8460af7b0b --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_pane_to_a_new_tab.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2849 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ tiled_pane ──────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ floating_pane_to_eject ──────────────┐ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-2.snap new file mode 100644 index 0000000000..85285fcf2d --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-2.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2986 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ tiled_pane ──────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ /path/to/fake/plugin ────────────────┐ │ +06 (C): │ │Loading /path/to/fake/plugin │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-3.snap new file mode 100644 index 0000000000..bf91ddbdfa --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-3.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2986 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ ┌ /path/to/fake/plugin ────────────────┐ │ +08 (C): │ │Loading /path/to/fake/plugin │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ │ │ │ +16 (C): │ └──────────────────────────────────────┘ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-4.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-4.snap new file mode 100644 index 0000000000..bf91ddbdfa --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-4.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2986 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ ┌ /path/to/fake/plugin ────────────────┐ │ +08 (C): │ │Loading /path/to/fake/plugin │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ │ │ │ +16 (C): │ └──────────────────────────────────────┘ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-5.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-5.snap new file mode 100644 index 0000000000..7af6aae61c --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-5.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2986 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ tiled_pane ──────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-6.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-6.snap new file mode 100644 index 0000000000..bf91ddbdfa --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-6.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2986 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ ┌ /path/to/fake/plugin ────────────────┐ │ +08 (C): │ │Loading /path/to/fake/plugin │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ │ │ │ +16 (C): │ └──────────────────────────────────────┘ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-7.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-7.snap new file mode 100644 index 0000000000..bf91ddbdfa --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-7.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2986 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ ┌ /path/to/fake/plugin ────────────────┐ │ +08 (C): │ │Loading /path/to/fake/plugin │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ │ │ │ +16 (C): │ └──────────────────────────────────────┘ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-8.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-8.snap new file mode 100644 index 0000000000..5c2254e143 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab-8.snap @@ -0,0 +1,6 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2988 +expression: "format!(\"{}\", snapshot_count)" +--- +7 diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab.snap new file mode 100644 index 0000000000..85285fcf2d --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_floating_plugin_pane_to_a_new_tab.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2986 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ tiled_pane ──────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ /path/to/fake/plugin ────────────────┐ │ +06 (C): │ │Loading /path/to/fake/plugin │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-2.snap new file mode 100644 index 0000000000..347d93d423 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-2.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2739 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_break_free ──────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-3.snap new file mode 100644 index 0000000000..2ee1fac443 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-3.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2739 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_stay ────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-4.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-4.snap new file mode 100644 index 0000000000..347d93d423 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-4.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2739 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_break_free ──────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-5.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-5.snap new file mode 100644 index 0000000000..ec0c6cfb5b --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab-5.snap @@ -0,0 +1,6 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2741 +expression: "format!(\"{}\", snapshot_count)" +--- +4 diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab.snap new file mode 100644 index 0000000000..765e54260b --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_pane_to_a_new_tab.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2739 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_break_free ──────────────────┐┌ pane_to_stay ────────────────────────┐ +01 (C): │ ││ │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └──────────────────────────────────────┘└──────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-2.snap new file mode 100644 index 0000000000..bbcf4b3aca --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-2.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2914 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_stay ────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-3.snap new file mode 100644 index 0000000000..e0a620f36d --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-3.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2914 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ plugin_pane_to_break_free ───────────────────────────────────────────────────┐ +01 (C): │Loading /path/to/fake/plugin │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-4.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-4.snap new file mode 100644 index 0000000000..e0a620f36d --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-4.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2914 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ plugin_pane_to_break_free ───────────────────────────────────────────────────┐ +01 (C): │Loading /path/to/fake/plugin │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-5.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-5.snap new file mode 100644 index 0000000000..bbcf4b3aca --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-5.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2914 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_stay ────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-6.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-6.snap new file mode 100644 index 0000000000..f9b6b71759 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab-6.snap @@ -0,0 +1,6 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2916 +expression: "format!(\"{}\", snapshot_count)" +--- +5 diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab.snap new file mode 100644 index 0000000000..4d4ae3f9e6 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_break_plugin_pane_to_a_new_tab.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2914 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ plugin_pane_to_break_free ───────────┐┌ pane_to_stay ────────────────────────┐ +01 (C): │Loading /path/to/fake/plugin ││ │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └──────────────────────────────────────┘└──────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-2.snap new file mode 100644 index 0000000000..bb34d594c7 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-2.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3079 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_break_free ──────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-3.snap new file mode 100644 index 0000000000..bcac6ce6ce --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-3.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3079 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_stay ────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-4.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-4.snap new file mode 100644 index 0000000000..593dd95bd6 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-4.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3079 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_stay ────────────────────────┐┌ pane_to_break_free ──────────────────┐ +01 (C): │ ││ │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └──────────────────────────────────────┘└──────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-5.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-5.snap new file mode 100644 index 0000000000..53aa1b2c03 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left-5.snap @@ -0,0 +1,6 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3081 +expression: "format!(\"{}\", snapshot_count)" +--- +4 diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left.snap new file mode 100644 index 0000000000..262029b40e --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_left.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3079 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_break_free ──────────────────┐┌ pane_to_stay ────────────────────────┐ +01 (C): │ ││ │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └──────────────────────────────────────┘└──────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-2.snap new file mode 100644 index 0000000000..32f3248e8a --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-2.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3032 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_break_free ──────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-3.snap new file mode 100644 index 0000000000..d1f54bfe96 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-3.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3032 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_stay ────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-4.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-4.snap new file mode 100644 index 0000000000..e9f22eb300 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-4.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3032 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_stay ────────────────────────┐┌ pane_to_break_free ──────────────────┐ +01 (C): │ ││ │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └──────────────────────────────────────┘└──────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-5.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-5.snap new file mode 100644 index 0000000000..da6e99bba9 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right-5.snap @@ -0,0 +1,6 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3034 +expression: "format!(\"{}\", snapshot_count)" +--- +4 diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right.snap new file mode 100644 index 0000000000..31169db365 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_can_move_pane_to_a_new_tab_right.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3032 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ pane_to_break_free ──────────────────┐┌ pane_to_stay ────────────────────────┐ +01 (C): │ ││ │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └──────────────────────────────────────┘└──────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_cannot_break_last_selectable_pane_to_a_new_tab-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_cannot_break_last_selectable_pane_to_a_new_tab-2.snap new file mode 100644 index 0000000000..93c4606dad --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_cannot_break_last_selectable_pane_to_a_new_tab-2.snap @@ -0,0 +1,6 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2775 +expression: "format!(\"{}\", snapshot_count)" +--- +1 diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_cannot_break_last_selectable_pane_to_a_new_tab.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_cannot_break_last_selectable_pane_to_a_new_tab.snap new file mode 100644 index 0000000000..ff7668a080 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__screen_cannot_break_last_selectable_pane_to_a_new_tab.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2773 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └──────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap index a25b01b471..0943801b24 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap @@ -1,5 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 2217 expression: "format!(\"{:#?}\", new_tab_action)" --- Some( @@ -23,6 +24,7 @@ Some( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -36,6 +38,7 @@ Some( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -46,6 +49,7 @@ Some( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ), [], diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap index cd5300d081..257ffddf45 100644 --- a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/./unit/screen_tests.rs -assertion_line: 2448 +assertion_line: 2263 expression: "format!(\"{:#?}\", new_tab_instruction)" --- NewTab( @@ -27,6 +27,7 @@ NewTab( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -44,6 +45,7 @@ NewTab( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -61,6 +63,7 @@ NewTab( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -71,6 +74,7 @@ NewTab( children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ), [], diff --git a/zellij-utils/assets/config/default.kdl b/zellij-utils/assets/config/default.kdl index 57106681d3..32dfb86e07 100644 --- a/zellij-utils/assets/config/default.kdl +++ b/zellij-utils/assets/config/default.kdl @@ -54,6 +54,9 @@ keybinds { bind "n" { NewTab; SwitchToMode "Normal"; } bind "x" { CloseTab; SwitchToMode "Normal"; } bind "s" { ToggleActiveSyncTab; SwitchToMode "Normal"; } + bind "b" { BreakPane; SwitchToMode "Normal"; } + bind "]" { BreakPaneRight; SwitchToMode "Normal"; } + bind "[" { BreakPaneLeft; SwitchToMode "Normal"; } bind "1" { GoToTab 1; SwitchToMode "Normal"; } bind "2" { GoToTab 2; SwitchToMode "Normal"; } bind "3" { GoToTab 3; SwitchToMode "Normal"; } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index f342d6780d..0058002930 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -339,6 +339,9 @@ pub enum ScreenContext { FocusPaneWithId, RenamePane, RenameTab, + BreakPane, + BreakPaneRight, + BreakPaneLeft, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 746624a890..236c9461fe 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -242,6 +242,9 @@ pub enum Action { RenameTerminalPane(u32, Vec), RenamePluginPane(u32, Vec), RenameTab(u32, Vec), + BreakPane, + BreakPaneRight, + BreakPaneLeft, } impl Action { diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 343431acf3..ed0b4a9d74 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -402,6 +402,7 @@ pub struct FloatingPaneLayout { pub y: Option, pub run: Option, pub focus: Option, + pub already_running: bool, } impl FloatingPaneLayout { @@ -439,6 +440,7 @@ pub struct TiledPaneLayout { pub children_are_stacked: bool, pub is_expanded_in_stack: bool, pub exclude_from_sync: Option, + pub run_instructions_to_ignore: Vec>, } impl TiledPaneLayout { @@ -560,8 +562,36 @@ impl TiledPaneLayout { let mut child_run_instructions = child.extract_run_instructions(); run_instructions.append(&mut child_run_instructions); } + let mut successfully_ignored = 0; + for instruction_to_ignore in &self.run_instructions_to_ignore { + if let Some(position) = run_instructions + .iter() + .position(|i| i == instruction_to_ignore) + { + run_instructions.remove(position); + successfully_ignored += 1; + } + } + // we need to do this because if we have an ignored instruction that does not match any + // running instruction, we'll have an extra pane and our state will be messed up and we'll + // crash (this can happen for example when breaking a plugin pane into a new tab that does + // not have room for it but has a terminal instead) + if successfully_ignored < self.run_instructions_to_ignore.len() { + for _ in 0..self + .run_instructions_to_ignore + .len() + .saturating_sub(successfully_ignored) + { + if let Some(position) = run_instructions.iter().position(|i| i == &None) { + run_instructions.remove(position); + } + } + } run_instructions } + pub fn ignore_run_instruction(&mut self, run_instruction: Option) { + self.run_instructions_to_ignore.push(run_instruction); + } pub fn with_one_pane() -> Self { let mut default_layout = TiledPaneLayout::default(); default_layout.children = vec![TiledPaneLayout::default()]; diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_added_to_args_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_added_to_args_in_template.snap index d01949514c..9d2d9e37e2 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_added_to_args_in_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_added_to_args_in_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1322 +assertion_line: 1352 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -34,6 +34,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -60,6 +61,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -70,6 +72,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_override_args_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_override_args_in_template.snap index d31f721f04..e8c140421a 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_override_args_in_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__args_override_args_in_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1287 +assertion_line: 1317 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -37,6 +37,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -63,6 +64,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -73,6 +75,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_a_stack_with_an_expanded_pane.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_a_stack_with_an_expanded_pane.snap index 31ea7cb7d2..83fb940433 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_a_stack_with_an_expanded_pane.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_a_stack_with_an_expanded_pane.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs +assertion_line: 1926 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -27,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -40,6 +42,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: true, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -53,6 +56,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -63,6 +67,7 @@ Layout { children_are_stacked: true, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -73,6 +78,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_stacked_children_for_pane_node.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_stacked_children_for_pane_node.snap index 85fa560d47..b82e9cd1e3 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_stacked_children_for_pane_node.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_stacked_children_for_pane_node.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1864 +assertion_line: 1894 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -28,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -41,6 +42,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -51,6 +53,7 @@ Layout { children_are_stacked: true, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -61,6 +64,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_stacked_children_for_pane_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_stacked_children_for_pane_template.snap index 2a6a8b2cf1..44da7ce78a 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_stacked_children_for_pane_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_define_stacked_children_for_pane_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1881 +assertion_line: 1911 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -32,6 +32,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -45,6 +46,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -55,6 +57,7 @@ Layout { children_are_stacked: true, is_expanded_in_stack: true, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -65,6 +68,7 @@ Layout { children_are_stacked: true, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -75,6 +79,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap index e9f849b7f6..07428ee0c9 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1863 +assertion_line: 1880 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -24,6 +24,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -34,6 +35,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -78,6 +80,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -99,6 +102,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -114,6 +118,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -124,6 +129,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -134,6 +140,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -165,6 +172,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -175,6 +183,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, MaxPanes( 8, @@ -212,6 +221,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -235,6 +245,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -252,6 +263,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -265,6 +277,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -278,6 +291,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -291,6 +305,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -301,6 +316,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -311,6 +327,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -321,6 +338,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -352,6 +370,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -362,6 +381,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, MaxPanes( 12, @@ -399,6 +419,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -422,6 +443,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -439,6 +461,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -452,6 +475,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -465,6 +489,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -478,6 +503,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -488,6 +514,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -505,6 +532,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -518,6 +546,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -531,6 +560,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -544,6 +574,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -554,6 +585,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -564,6 +596,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -574,6 +607,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -605,6 +639,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -615,6 +650,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, }, Some( diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__children_not_as_first_child_of_pane_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__children_not_as_first_child_of_pane_template.snap index 9308614a63..4bb0487202 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__children_not_as_first_child_of_pane_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__children_not_as_first_child_of_pane_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 956 +assertion_line: 975 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -34,6 +34,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -51,6 +52,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -64,6 +66,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -74,6 +77,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -84,6 +88,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -97,6 +102,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -107,6 +113,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -128,6 +135,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -140,6 +148,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -153,6 +162,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -163,6 +173,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -173,6 +184,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__children_not_as_first_child_of_tab_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__children_not_as_first_child_of_tab_template.snap index 7ff5948618..c5f538f968 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__children_not_as_first_child_of_tab_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__children_not_as_first_child_of_tab_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 908 +assertion_line: 927 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -29,6 +29,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -46,6 +47,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -59,6 +61,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -69,6 +72,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -79,6 +83,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -92,6 +97,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -102,6 +108,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -127,6 +134,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -139,6 +147,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -152,6 +161,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -162,6 +172,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -181,6 +192,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_added_to_close_on_exit_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_added_to_close_on_exit_in_template.snap index 403980b6dd..fdd0b7f0c1 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_added_to_close_on_exit_in_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_added_to_close_on_exit_in_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1339 +assertion_line: 1369 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -34,6 +34,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -57,6 +58,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -67,6 +69,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_overrides_close_on_exit_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_overrides_close_on_exit_in_template.snap index 23a71d4483..3a4092cff9 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_overrides_close_on_exit_in_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__close_on_exit_overrides_close_on_exit_in_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1305 +assertion_line: 1335 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -34,6 +34,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -57,6 +58,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -67,6 +69,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__combined_tab_and_pane_template_both_with_children.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__combined_tab_and_pane_template_both_with_children.snap index 80ba99b1f8..0f0a62ac0e 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__combined_tab_and_pane_template_both_with_children.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__combined_tab_and_pane_template_both_with_children.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1008 +assertion_line: 1027 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -29,6 +29,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -48,6 +49,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -58,6 +60,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -71,6 +74,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -81,6 +85,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -98,6 +103,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -111,6 +117,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -121,6 +128,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -131,6 +139,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -156,6 +165,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -175,6 +185,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -185,6 +196,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -198,6 +210,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -208,6 +221,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -221,6 +235,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -231,6 +246,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -250,6 +266,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_added_to_cwd_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_added_to_cwd_in_template.snap index 532959f422..469eb9b977 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_added_to_cwd_in_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_added_to_cwd_in_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1374 +assertion_line: 1404 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -34,6 +34,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -59,6 +60,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -69,6 +71,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_override_cwd_in_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_override_cwd_in_template.snap index 54d78e9804..42d8bcf0c9 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_override_cwd_in_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__cwd_override_cwd_in_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1357 +assertion_line: 1387 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -36,6 +36,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -61,6 +62,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -71,6 +73,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap index de30d22961..6691e25e34 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__env_var_expansion.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 2116 +assertion_line: 2141 expression: "format!(\"{layout:#?}\")" --- Layout { @@ -28,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -45,6 +46,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -62,6 +64,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -79,6 +82,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -104,6 +108,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -125,6 +130,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -146,6 +152,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -171,6 +178,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -192,6 +200,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -202,6 +211,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd.snap index 81673d25b6..6060b071b5 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1759 +assertion_line: 1789 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -27,6 +27,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -52,6 +53,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -66,6 +68,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -85,6 +88,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_pane_templates.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_pane_templates.snap index c87db71c77..723af76229 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_pane_templates.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_pane_templates.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1780 +assertion_line: 1810 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -31,6 +31,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -56,6 +57,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -77,6 +79,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -91,6 +94,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -105,6 +109,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -119,6 +124,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -138,6 +144,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_tab_templates.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_tab_templates.snap index 321f36e49d..de740071ad 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_tab_templates.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_and_tab_cwd_prepended_to_panes_with_and_without_cwd_in_tab_templates.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1799 +assertion_line: 1829 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -27,6 +27,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -52,6 +53,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -73,6 +75,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -87,6 +90,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -101,6 +105,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -120,6 +125,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap index bd9888fef4..72f9e14e33 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_given_to_panes_without_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1644 +assertion_line: 1674 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -28,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -53,6 +54,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -63,6 +65,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap index f871ff739d..b6a29005b3 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1677 +assertion_line: 1707 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -28,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -53,6 +54,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -63,6 +65,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap index d7b8582218..c673885a68 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_passed_from_layout_constructor_overrides_global_cwd_in_layout_file.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1698 +assertion_line: 1728 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -28,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -53,6 +54,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -63,6 +65,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap index 770a43cc3c..41678b395c 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_prepended_to_panes_with_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1657 +assertion_line: 1687 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -28,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -53,6 +54,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -63,6 +65,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_with_tab_cwd_given_to_panes_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_with_tab_cwd_given_to_panes_without_cwd.snap index b9829595ce..d386d9c94b 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_with_tab_cwd_given_to_panes_without_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__global_cwd_with_tab_cwd_given_to_panes_without_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1714 +assertion_line: 1744 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -27,6 +27,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -52,6 +53,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -66,6 +68,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -85,6 +88,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_close_on_exit.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_close_on_exit.snap index 4f5c5e0bb1..b213ea2115 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_close_on_exit.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_close_on_exit.snap @@ -34,6 +34,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -44,6 +45,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_start_suspended.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_start_suspended.snap index 9f118820a5..35c343730c 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_start_suspended.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_command_panes_and_start_suspended.snap @@ -34,6 +34,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -44,6 +45,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_default_tab_template.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_default_tab_template.snap index bec76e1184..92e58f7d97 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_default_tab_template.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_default_tab_template.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 767 +assertion_line: 786 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -25,6 +25,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Vertical, @@ -42,6 +43,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -55,6 +57,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -65,6 +68,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -78,6 +82,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -88,6 +93,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -111,6 +117,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -128,6 +135,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -141,6 +149,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -151,6 +160,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -164,6 +174,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -174,6 +185,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -195,6 +207,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -208,6 +221,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -221,6 +235,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -231,6 +246,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -254,6 +270,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -267,6 +284,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -280,6 +298,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -290,6 +309,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_nested_branched_pane_templates.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_nested_branched_pane_templates.snap index d3b743e17e..707ec26822 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_nested_branched_pane_templates.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_nested_branched_pane_templates.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 862 +assertion_line: 881 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -28,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -41,6 +42,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -58,6 +60,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -71,6 +74,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -81,6 +85,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -98,6 +103,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -111,6 +117,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -124,6 +131,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -134,6 +142,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -144,6 +153,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -154,6 +164,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_nested_pane_templates.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_nested_pane_templates.snap index 8a6dce5223..ce8ee0a96d 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_nested_pane_templates.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_nested_pane_templates.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 836 +assertion_line: 855 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -28,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -41,6 +42,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -58,6 +60,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -71,6 +74,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -81,6 +85,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -94,6 +99,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -104,6 +110,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -114,6 +121,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_pane_excluded_from_sync.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_pane_excluded_from_sync.snap index 25f7dc0764..554420b1dd 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_pane_excluded_from_sync.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_pane_excluded_from_sync.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs +assertion_line: 1038 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -25,6 +26,7 @@ Layout { exclude_from_sync: Some( true, ), + run_instructions_to_ignore: [], }, ], split_size: None, @@ -35,6 +37,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_pane_templates.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_pane_templates.snap index ff18632fd5..0eeb76af4d 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_pane_templates.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_pane_templates.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 794 +assertion_line: 813 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -28,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -45,6 +46,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -55,6 +57,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -68,6 +71,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -78,6 +82,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Vertical, @@ -95,6 +100,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -112,6 +118,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -125,6 +132,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -135,6 +143,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -148,6 +157,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -158,6 +168,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Vertical, @@ -175,6 +186,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Vertical, @@ -192,6 +204,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -205,6 +218,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -215,6 +229,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -228,6 +243,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -238,6 +254,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Vertical, @@ -255,6 +272,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -268,6 +286,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -281,6 +300,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -291,6 +311,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -301,6 +322,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tab_and_pane_templates.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tab_and_pane_templates.snap index 78f47b4935..1bc1b01c64 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tab_and_pane_templates.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tab_and_pane_templates.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 815 +assertion_line: 834 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -27,6 +27,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -54,6 +55,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -64,6 +66,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -77,6 +80,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -87,6 +91,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -97,6 +102,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -116,6 +122,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tabs_and_floating_panes.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tabs_and_floating_panes.snap index b993cec3e0..c9776160e4 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tabs_and_floating_panes.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__layout_with_tabs_and_floating_panes.snap @@ -23,6 +23,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -33,6 +34,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [ FloatingPaneLayout { @@ -43,6 +45,7 @@ Layout { y: None, run: None, focus: None, + already_running: false, }, ], ), @@ -64,6 +67,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -74,6 +78,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [ FloatingPaneLayout { @@ -84,6 +89,7 @@ Layout { y: None, run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -93,6 +99,7 @@ Layout { y: None, run: None, focus: None, + already_running: false, }, ], ), @@ -112,6 +119,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap index 849b9b6d1f..a8bff78ed8 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_is_overriden_by_its_consumers_bare_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1528 +assertion_line: 1558 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -36,6 +36,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -46,6 +47,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap index e7b200fd55..433283fcda 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_overriden_by_its_consumers_command_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1474 +assertion_line: 1504 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -36,6 +36,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -46,6 +47,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap index ed4a906b4e..8d06ac560b 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_with_cwd_remains_when_its_consumer_command_does_not_have_a_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1491 +assertion_line: 1521 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -36,6 +36,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -46,6 +47,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap index 0fc98b67bd..5d7f6b9918 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_is_overriden_by_its_consumers_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1509 +assertion_line: 1539 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -36,6 +36,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -46,6 +47,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap index 8f0411100e..f49d65f4f8 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_command_without_cwd_receives_its_consumers_bare_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1546 +assertion_line: 1576 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -36,6 +36,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -46,6 +47,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_cwd_overriden_by_its_consumers_bare_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_cwd_overriden_by_its_consumers_bare_cwd.snap index 9fc4aaf122..cf7c7b7a31 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_cwd_overriden_by_its_consumers_bare_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_cwd_overriden_by_its_consumers_bare_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1564 +assertion_line: 1594 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -28,6 +28,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -38,6 +39,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap index d0125554fb..c55fb95213 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_with_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1598 +assertion_line: 1628 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -36,6 +36,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -46,6 +47,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap index d9bbf77a88..34280469a2 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_command_without_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1580 +assertion_line: 1610 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -36,6 +36,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -46,6 +47,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_edit.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_edit.snap index be2f0fb428..faac5c46d3 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_edit.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_bare_propagated_to_its_consumer_edit.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs +assertion_line: 1644 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -31,6 +32,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -41,6 +43,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_command_propagated_to_its_consumer_edit.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_command_propagated_to_its_consumer_edit.snap index a33fc2bdac..5930f95184 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_command_propagated_to_its_consumer_edit.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__pane_template_with_command_propagated_to_its_consumer_edit.snap @@ -1,5 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs +assertion_line: 1660 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -31,6 +32,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -41,6 +43,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_given_to_panes_without_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_given_to_panes_without_cwd.snap index a74656108e..98b7a5cd8e 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_given_to_panes_without_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_given_to_panes_without_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1729 +assertion_line: 1759 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -27,6 +27,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -52,6 +53,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -66,6 +68,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -85,6 +88,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_prepended_to_panes_with_cwd.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_prepended_to_panes_with_cwd.snap index 65ed2b5031..5ca79f19e2 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_prepended_to_panes_with_cwd.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__tab_cwd_prepended_to_panes_with_cwd.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1744 +assertion_line: 1774 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -27,6 +27,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -52,6 +53,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -66,6 +68,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -85,6 +88,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 1402b5eff9..30513992eb 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -922,6 +922,9 @@ impl TryFrom<(&KdlNode, &Options)> for Action { }, "PreviousSwapLayout" => Ok(Action::PreviousSwapLayout), "NextSwapLayout" => Ok(Action::NextSwapLayout), + "BreakPane" => Ok(Action::BreakPane), + "BreakPaneRight" => Ok(Action::BreakPaneRight), + "BreakPaneLeft" => Ok(Action::BreakPaneLeft), _ => Err(ConfigError::new_kdl_error( format!("Unsupported action: {}", action_name).into(), kdl_action.span().offset(), diff --git a/zellij-utils/src/pane_size.rs b/zellij-utils/src/pane_size.rs index 8e68c3863b..afa0fa9d66 100644 --- a/zellij-utils/src/pane_size.rs +++ b/zellij-utils/src/pane_size.rs @@ -25,6 +25,12 @@ pub struct Viewport { pub cols: usize, } +impl Viewport { + pub fn has_positive_size(&self) -> bool { + self.rows > 0 && self.cols > 0 + } +} + #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct Offset { pub top: usize, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options-2.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options-2.snap index 452f0a8be5..a3d69ce85b 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options-2.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options-2.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/setup.rs -assertion_line: 665 +assertion_line: 683 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -20,6 +20,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-2.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-2.snap index e294f81732..f68f6c5baa 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-2.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-2.snap @@ -42,6 +42,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -55,6 +56,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -86,6 +88,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -96,6 +99,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), @@ -140,6 +144,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -161,6 +166,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -176,6 +182,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -186,6 +193,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -196,6 +204,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -227,6 +236,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -237,6 +247,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, MaxPanes( 8, @@ -274,6 +285,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -297,6 +309,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -314,6 +327,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -327,6 +341,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -340,6 +355,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -353,6 +369,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -363,6 +380,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -373,6 +391,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -383,6 +402,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -414,6 +434,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -424,6 +445,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, MaxPanes( 12, @@ -461,6 +483,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -484,6 +507,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -501,6 +525,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -514,6 +539,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -527,6 +553,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -540,6 +567,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -550,6 +578,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -567,6 +596,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -580,6 +610,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -593,6 +624,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -606,6 +638,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -616,6 +649,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -626,6 +660,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -636,6 +671,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -667,6 +703,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -677,6 +714,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, }, Some( @@ -721,6 +759,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -738,6 +777,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -751,6 +791,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -761,6 +802,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -792,6 +834,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -802,6 +845,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, MaxPanes( 8, @@ -839,6 +883,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -862,6 +907,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Vertical, @@ -879,6 +925,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -892,6 +939,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -905,6 +953,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -918,6 +967,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -928,6 +978,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -938,6 +989,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -948,6 +1000,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -979,6 +1032,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -989,6 +1043,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, MaxPanes( 12, @@ -1026,6 +1081,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1049,6 +1105,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Vertical, @@ -1066,6 +1123,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1079,6 +1137,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1092,6 +1151,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1105,6 +1165,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -1115,6 +1176,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Vertical, @@ -1132,6 +1194,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1145,6 +1208,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1158,6 +1222,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1171,6 +1236,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -1181,6 +1247,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -1191,6 +1258,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -1201,6 +1269,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1232,6 +1301,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -1242,6 +1312,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, }, Some( @@ -1286,6 +1357,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1307,6 +1379,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1322,6 +1395,7 @@ Layout { children_are_stacked: true, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -1332,6 +1406,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -1342,6 +1417,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, TiledPaneLayout { children_split_direction: Horizontal, @@ -1373,6 +1449,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, ], split_size: None, @@ -1383,6 +1460,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, }, Some( @@ -1428,6 +1506,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1453,6 +1532,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1478,6 +1558,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1503,6 +1584,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1528,6 +1610,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1553,6 +1636,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1578,6 +1662,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1603,6 +1688,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1628,6 +1714,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1655,6 +1742,7 @@ Layout { focus: Some( true, ), + already_running: false, }, ], }, @@ -1683,6 +1771,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, ], MaxPanes( @@ -1708,6 +1797,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1729,6 +1819,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, ], MaxPanes( @@ -1756,6 +1847,7 @@ Layout { focus: Some( true, ), + already_running: false, }, FloatingPaneLayout { name: None, @@ -1777,6 +1869,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1798,6 +1891,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, ], MaxPanes( @@ -1827,6 +1921,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1854,6 +1949,7 @@ Layout { focus: Some( true, ), + already_running: false, }, FloatingPaneLayout { name: None, @@ -1879,6 +1975,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, FloatingPaneLayout { name: None, @@ -1904,6 +2001,7 @@ Layout { ), run: None, focus: None, + already_running: false, }, ], }, 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 23da443fc1..7f681757df 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 @@ -993,6 +993,30 @@ Config { Normal, ), ], + Char( + '[', + ): [ + BreakPaneLeft, + SwitchToMode( + Normal, + ), + ], + Char( + ']', + ): [ + BreakPaneRight, + SwitchToMode( + Normal, + ), + ], + Char( + 'b', + ): [ + BreakPane, + SwitchToMode( + Normal, + ), + ], Char( 'h', ): [ 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 e5f37d87a4..f8cbc41322 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 @@ -993,6 +993,30 @@ Config { Normal, ), ], + Char( + '[', + ): [ + BreakPaneLeft, + SwitchToMode( + Normal, + ), + ], + Char( + ']', + ): [ + BreakPaneRight, + SwitchToMode( + Normal, + ), + ], + Char( + 'b', + ): [ + BreakPane, + SwitchToMode( + Normal, + ), + ], Char( 'h', ): [ diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options-2.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options-2.snap index 4caba6fbe7..4f5321b166 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options-2.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options-2.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/setup.rs -assertion_line: 647 +assertion_line: 665 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -20,6 +20,7 @@ Layout { children_are_stacked: false, is_expanded_in_stack: false, exclude_from_sync: None, + run_instructions_to_ignore: [], }, [], ), diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap index 5540a57105..435016634a 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap @@ -993,6 +993,30 @@ Config { Normal, ), ], + Char( + '[', + ): [ + BreakPaneLeft, + SwitchToMode( + Normal, + ), + ], + Char( + ']', + ): [ + BreakPaneRight, + SwitchToMode( + Normal, + ), + ], + Char( + 'b', + ): [ + BreakPane, + SwitchToMode( + Normal, + ), + ], Char( 'h', ): [ 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 b885dafffc..4159c5254f 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 @@ -993,6 +993,30 @@ Config { Normal, ), ], + Char( + '[', + ): [ + BreakPaneLeft, + SwitchToMode( + Normal, + ), + ], + Char( + ']', + ): [ + BreakPaneRight, + SwitchToMode( + Normal, + ), + ], + Char( + 'b', + ): [ + BreakPane, + SwitchToMode( + Normal, + ), + ], Char( 'h', ): [ 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 b371b61ff3..ebf3c501e2 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 @@ -993,6 +993,30 @@ Config { Normal, ), ], + Char( + '[', + ): [ + BreakPaneLeft, + SwitchToMode( + Normal, + ), + ], + Char( + ']', + ): [ + BreakPaneRight, + SwitchToMode( + Normal, + ), + ], + Char( + 'b', + ): [ + BreakPane, + SwitchToMode( + Normal, + ), + ], Char( 'h', ): [ From 65d361b5cc492b19580040b064d2c182e984073d Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 2 Aug 2023 11:42:55 +0200 Subject: [PATCH 025/128] docs(changelog): break pane to new tab --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa957baaff..bbd4495f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * feat(plugins): make plugins configurable (https://github.com/zellij-org/zellij/pull/2646) * fix(terminal): occasional glitches while changing focus (https://github.com/zellij-org/zellij/pull/2654) * feat(plugins): add utility functions to get focused tab/pane (https://github.com/zellij-org/zellij/pull/2652) +* feat(ui): break pane to new tab and move panes between tabs (https://github.com/zellij-org/zellij/pull/2664) ## [0.37.2] - 2023-06-20 * hotfix: include theme files into binary (https://github.com/zellij-org/zellij/pull/2566) From 4b9454fa8c7c4e3736b03e14fd248f9ad2e750fb Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Fri, 4 Aug 2023 10:22:46 +0200 Subject: [PATCH 026/128] fix(performance): plug memory leak (#2675) --- zellij-server/src/output/mod.rs | 4 +- zellij-server/src/panes/grid.rs | 80 +++++++++---------- zellij-server/src/ui/pane_boundaries_frame.rs | 8 +- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/zellij-server/src/output/mod.rs b/zellij-server/src/output/mod.rs index a2d0419701..6f2b295bbc 100644 --- a/zellij-server/src/output/mod.rs +++ b/zellij-server/src/output/mod.rs @@ -874,7 +874,7 @@ impl OutputBuffer { y_offset: usize, ) -> Vec { if self.should_update_all_lines { - let mut changed_chunks = Vec::with_capacity(viewport.len()); + let mut changed_chunks = Vec::new(); for line_index in 0..viewport_height { let terminal_characters = self.extract_line_from_viewport(line_index, viewport, viewport_width); @@ -888,7 +888,7 @@ impl OutputBuffer { let mut line_changes = self.changed_lines.to_vec(); line_changes.sort_unstable(); line_changes.dedup(); - let mut changed_chunks = Vec::with_capacity(line_changes.len()); + let mut changed_chunks = Vec::new(); for line_index in line_changes { let terminal_characters = self.extract_line_from_viewport(line_index, viewport, viewport_width); diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index 3ae47327a6..d63fac5621 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -135,8 +135,8 @@ fn transfer_rows_from_lines_above_to_viewport( lines_added_to_viewport -= top_non_canonical_rows_in_dst.len() as isize; next_lines.push(next_line); next_lines.append(&mut top_non_canonical_rows_in_dst); - next_lines = Row::from_rows(next_lines, max_viewport_width) - .split_to_rows_of_length(max_viewport_width); + next_lines = + Row::from_rows(next_lines).split_to_rows_of_length(max_viewport_width); if next_lines.is_empty() { // no more lines at lines_above, the line we popped was probably empty break; @@ -149,7 +149,7 @@ fn transfer_rows_from_lines_above_to_viewport( lines_added_to_viewport += 1; } if !next_lines.is_empty() { - let excess_row = Row::from_rows(next_lines, 0); + let excess_row = Row::from_rows(next_lines); bounded_push(lines_above, sixel_grid, excess_row); } match usize::try_from(lines_added_to_viewport) { @@ -179,7 +179,7 @@ fn transfer_rows_from_viewport_to_lines_above( next_lines.append(&mut bottom_canonical_row_and_wraps_in_dst); } next_lines.push(next_line); - next_lines = vec![Row::from_rows(next_lines, 0)]; + next_lines = vec![Row::from_rows(next_lines)]; } else { break; // no more rows } @@ -191,8 +191,7 @@ fn transfer_rows_from_viewport_to_lines_above( } } if !next_lines.is_empty() { - let excess_rows = Row::from_rows(next_lines, max_viewport_width) - .split_to_rows_of_length(max_viewport_width); + let excess_rows = Row::from_rows(next_lines).split_to_rows_of_length(max_viewport_width); for row in excess_rows { viewport.insert(0, row); } @@ -217,12 +216,12 @@ fn transfer_rows_from_lines_below_to_viewport( let mut canonical_line = get_viewport_bottom_canonical_row_and_wraps(viewport); lines_pulled_from_viewport += canonical_line.len(); canonical_line.append(&mut top_non_canonical_rows_in_lines_below); - next_lines = Row::from_rows(canonical_line, max_viewport_width) - .split_to_rows_of_length(max_viewport_width); + next_lines = + Row::from_rows(canonical_line).split_to_rows_of_length(max_viewport_width); } else { let canonical_row = get_top_canonical_row_and_wraps(lines_below); - next_lines = Row::from_rows(canonical_row, max_viewport_width) - .split_to_rows_of_length(max_viewport_width); + next_lines = + Row::from_rows(canonical_row).split_to_rows_of_length(max_viewport_width); } } else { break; // no more rows @@ -235,7 +234,7 @@ fn transfer_rows_from_lines_below_to_viewport( } } if !next_lines.is_empty() { - let excess_row = Row::from_rows(next_lines, 0); + let excess_row = Row::from_rows(next_lines); lines_below.insert(0, excess_row); } } @@ -404,7 +403,7 @@ impl Debug for Grid { let mut buffer: Vec = self.viewport.clone(); // pad buffer for _ in buffer.len()..self.height { - buffer.push(Row::new(self.width).canonical()); + buffer.push(Row::new().canonical()); } // display sixel placeholder @@ -461,13 +460,14 @@ impl Grid { debug: bool, ) -> Self { let sixel_grid = SixelGrid::new(character_cell_size.clone(), sixel_image_store); + // make sure this is initialized as it is used internally + // if it was already initialized (which should happen normally unless this is a test or + // something changed since this comment was written), we get an Error which we ignore + // I don't know why this needs to be a OneCell, but whatevs + let _ = SCROLL_BUFFER_SIZE.set(DEFAULT_SCROLL_BUFFER_SIZE); Grid { - lines_above: VecDeque::with_capacity( - // .get_or_init() is used instead of .get().unwrap() to prevent - // unit tests from panicking where SCROLL_BUFFER_SIZE is uninitialized. - *SCROLL_BUFFER_SIZE.get_or_init(|| DEFAULT_SCROLL_BUFFER_SIZE), - ), - viewport: vec![Row::new(columns).canonical()], + lines_above: VecDeque::new(), + viewport: vec![Row::new().canonical()], lines_below: vec![], horizontal_tabstops: create_horizontal_tabstops(columns), cursor: Cursor::new(0, 0), @@ -823,7 +823,7 @@ impl Grid { for mut canonical_line in viewport_canonical_lines { let mut canonical_line_parts: Vec = vec![]; if canonical_line.columns.is_empty() { - canonical_line_parts.push(Row::new(new_columns).canonical()); + canonical_line_parts.push(Row::new().canonical()); } while !canonical_line.columns.is_empty() { let next_wrap = canonical_line.drain_until(new_columns); @@ -1248,7 +1248,7 @@ impl Grid { // FIXME: this should add an empty line with the pad_character // but for some reason this breaks rendering in various situations // it needs to be investigated and fixed - let new_row = Row::new(self.width).canonical(); + let new_row = Row::new().canonical(); self.viewport.push(new_row); } if self.cursor.y == self.height.saturating_sub(1) { @@ -1314,13 +1314,10 @@ impl Grid { None => { // pad lines until cursor if they do not exist for _ in self.viewport.len()..self.cursor.y { - self.viewport.push(Row::new(self.width).canonical()); + self.viewport.push(Row::new().canonical()); } - self.viewport.push( - Row::new(self.width) - .with_character(terminal_character) - .canonical(), - ); + self.viewport + .push(Row::new().with_character(terminal_character).canonical()); self.output_buffer.update_line(self.cursor.y); }, } @@ -1409,14 +1406,14 @@ impl Grid { } else { self.viewport.remove(0); } - let wrapped_row = Row::new(self.width); + let wrapped_row = Row::new(); self.viewport.push(wrapped_row); self.selection.move_up(1); self.output_buffer.update_all_lines(); } else { self.cursor.y += 1; if self.viewport.len() <= self.cursor.y { - let line_wrapped_row = Row::new(self.width); + let line_wrapped_row = Row::new(); self.viewport.push(line_wrapped_row); self.output_buffer.update_line(self.cursor.y); } @@ -1494,8 +1491,7 @@ impl Grid { if scroll_region_bottom < self.viewport.len() { self.viewport.remove(scroll_region_bottom); } - self.viewport - .insert(current_line_index, Row::new(self.width)); // TODO: .canonical() ? + self.viewport.insert(current_line_index, Row::new()); // TODO: .canonical() ? } else if current_line_index > scroll_region_top && current_line_index <= scroll_region_bottom { @@ -1654,9 +1650,9 @@ impl Grid { self.should_render = true; } pub fn reset_terminal_state(&mut self) { - self.lines_above = VecDeque::with_capacity(*SCROLL_BUFFER_SIZE.get().unwrap()); + self.lines_above = VecDeque::new(); self.lines_below = vec![]; - self.viewport = vec![Row::new(self.width).canonical()]; + self.viewport = vec![Row::new().canonical()]; self.alternate_screen_state = None; self.cursor_key_mode = false; self.scroll_region = None; @@ -2571,14 +2567,10 @@ impl Perform for Grid { }, 1049 => { // enter alternate buffer - let current_lines_above = std::mem::replace( - &mut self.lines_above, - VecDeque::with_capacity(*SCROLL_BUFFER_SIZE.get().unwrap()), - ); - let current_viewport = std::mem::replace( - &mut self.viewport, - vec![Row::new(self.width).canonical()], - ); + let current_lines_above = + std::mem::replace(&mut self.lines_above, VecDeque::new()); + let current_viewport = + std::mem::replace(&mut self.viewport, vec![Row::new().canonical()]); let current_cursor = std::mem::replace(&mut self.cursor, Cursor::new(0, 0)); let sixel_image_store = self.sixel_grid.sixel_image_store.clone(); @@ -3057,9 +3049,9 @@ impl Debug for Row { } impl Row { - pub fn new(width: usize) -> Self { + pub fn new() -> Self { Row { - columns: VecDeque::with_capacity(width), + columns: VecDeque::new(), is_canonical: false, width: None, } @@ -3071,9 +3063,9 @@ impl Row { width: None, } } - pub fn from_rows(mut rows: Vec, width: usize) -> Self { + pub fn from_rows(mut rows: Vec) -> Self { if rows.is_empty() { - Row::new(width) + Row::new() } else { let mut first_row = rows.remove(0); for row in &mut rows { diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index 7f8d801712..94569ccacb 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -9,7 +9,7 @@ use zellij_utils::pane_size::Viewport; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; fn foreground_color(characters: &str, color: Option) -> Vec { - let mut colored_string = Vec::with_capacity(characters.chars().count()); + let mut colored_string = Vec::new(); for character in characters.chars() { let styles = match color { Some(palette_color) => { @@ -36,7 +36,7 @@ fn foreground_color(characters: &str, color: Option) -> Vec) -> Vec { - let mut colored_string = Vec::with_capacity(characters.chars().count()); + let mut colored_string = Vec::new(); for character in characters.chars() { let styles = match color { Some(palette_color) => { @@ -367,7 +367,7 @@ impl PaneFrame { } else { let length_of_each_half = (max_length - middle_truncated_sign.width()) / 2; - let mut first_part: String = String::with_capacity(length_of_each_half); + let mut first_part: String = String::new(); for char in full_text.chars() { if first_part.width() + char.width().unwrap_or(0) > length_of_each_half { break; @@ -376,7 +376,7 @@ impl PaneFrame { } } - let mut second_part: String = String::with_capacity(length_of_each_half); + let mut second_part: String = String::new(); for char in full_text.chars().rev() { if second_part.width() + char.width().unwrap_or(0) > length_of_each_half { break; From 9813418ce36746f3b0b4fb8dc0de349461630ee3 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Fri, 4 Aug 2023 10:23:45 +0200 Subject: [PATCH 027/128] docs(changelog): plug memory leak --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbd4495f43..17f10d4834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * fix(terminal): occasional glitches while changing focus (https://github.com/zellij-org/zellij/pull/2654) * feat(plugins): add utility functions to get focused tab/pane (https://github.com/zellij-org/zellij/pull/2652) * feat(ui): break pane to new tab and move panes between tabs (https://github.com/zellij-org/zellij/pull/2664) +* fix(performance): plug memory leak (https://github.com/zellij-org/zellij/pull/2675) ## [0.37.2] - 2023-06-20 * hotfix: include theme files into binary (https://github.com/zellij-org/zellij/pull/2566) From 4d498d424416e50bf7c5e326400eb9bc99ff1b32 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 9 Aug 2023 22:26:00 +0200 Subject: [PATCH 028/128] feat(plugins): use protocol buffers for serializing across the wasm boundary (#2686) * work * almost done with command protobuffers * done translating command data structures * mid transferring of every command to protobuff command * transferred plugin_command.rs, now moving on to shim.rs * plugin command working with protobufs * protobuffers in update * protobuf event tests * various TODOs and comments * fix zellij-tile * clean up prost deps * remove version mismatch error * fix panic * some cleanups * clean up event protobuffers * clean up command protobuffers * clean up various protobufs * refactor protobufs * update comments * some transformation fixes * use protobufs for workers * style(fmt): rustfmt * style(fmt): rustfmt * chore(build): add protoc * chore(build): authenticate protoc --- .github/workflows/e2e.yml | 4 + .github/workflows/release.yml | 5 + .github/workflows/rust.yml | 13 + Cargo.lock | 97 ++ .../fixture-plugin-for-tests/src/main.rs | 61 +- default-plugins/strider/src/main.rs | 80 +- default-plugins/strider/src/search/mod.rs | 28 +- .../strider/src/search/search_state.rs | 46 +- default-plugins/strider/src/state.rs | 5 +- zellij-server/src/plugins/plugin_loader.rs | 131 +- zellij-server/src/plugins/plugin_worker.rs | 31 +- ..._tests__go_to_tab_name_plugin_command.snap | 4 +- ..._tests__send_configuration_to_plugins.snap | 4 +- ...gin_tests__write_chars_plugin_command.snap | 4 +- zellij-server/src/plugins/wasm_bridge.rs | 29 +- zellij-server/src/plugins/zellij_exports.rs | 1045 ++++++------- zellij-tile/src/lib.rs | 50 +- zellij-tile/src/shim.rs | 529 ++++--- zellij-utils/Cargo.toml | 4 + zellij-utils/assets/plugins/compact-bar.wasm | Bin 526958 -> 526730 bytes zellij-utils/assets/plugins/tab-bar.wasm | Bin 497627 -> 497583 bytes zellij-utils/build.rs | 21 + zellij-utils/src/data.rs | 145 +- zellij-utils/src/lib.rs | 4 +- zellij-utils/src/plugin_api/action.proto | 246 ++++ zellij-utils/src/plugin_api/action.rs | 1304 +++++++++++++++++ zellij-utils/src/plugin_api/command.proto | 9 + zellij-utils/src/plugin_api/command.rs | 26 + zellij-utils/src/plugin_api/event.proto | 162 ++ zellij-utils/src/plugin_api/event.rs | 1059 +++++++++++++ zellij-utils/src/plugin_api/file.proto | 9 + zellij-utils/src/plugin_api/file.rs | 30 + zellij-utils/src/plugin_api/input_mode.proto | 40 + zellij-utils/src/plugin_api/input_mode.rs | 69 + zellij-utils/src/plugin_api/key.proto | 83 ++ zellij-utils/src/plugin_api/key.rs | 222 +++ zellij-utils/src/plugin_api/message.proto | 9 + zellij-utils/src/plugin_api/message.rs | 29 + zellij-utils/src/plugin_api/mod.rs | 14 + .../src/plugin_api/plugin_command.proto | 175 +++ zellij-utils/src/plugin_api/plugin_command.rs | 825 +++++++++++ zellij-utils/src/plugin_api/plugin_ids.proto | 12 + zellij-utils/src/plugin_api/plugin_ids.rs | 35 + zellij-utils/src/plugin_api/resize.proto | 24 + zellij-utils/src/plugin_api/resize.rs | 130 ++ zellij-utils/src/plugin_api/style.proto | 54 + zellij-utils/src/plugin_api/style.rs | 213 +++ 47 files changed, 6046 insertions(+), 1073 deletions(-) create mode 100644 zellij-utils/build.rs create mode 100644 zellij-utils/src/plugin_api/action.proto create mode 100644 zellij-utils/src/plugin_api/action.rs create mode 100644 zellij-utils/src/plugin_api/command.proto create mode 100644 zellij-utils/src/plugin_api/command.rs create mode 100644 zellij-utils/src/plugin_api/event.proto create mode 100644 zellij-utils/src/plugin_api/event.rs create mode 100644 zellij-utils/src/plugin_api/file.proto create mode 100644 zellij-utils/src/plugin_api/file.rs create mode 100644 zellij-utils/src/plugin_api/input_mode.proto create mode 100644 zellij-utils/src/plugin_api/input_mode.rs create mode 100644 zellij-utils/src/plugin_api/key.proto create mode 100644 zellij-utils/src/plugin_api/key.rs create mode 100644 zellij-utils/src/plugin_api/message.proto create mode 100644 zellij-utils/src/plugin_api/message.rs create mode 100644 zellij-utils/src/plugin_api/mod.rs create mode 100644 zellij-utils/src/plugin_api/plugin_command.proto create mode 100644 zellij-utils/src/plugin_api/plugin_command.rs create mode 100644 zellij-utils/src/plugin_api/plugin_ids.proto create mode 100644 zellij-utils/src/plugin_api/plugin_ids.rs create mode 100644 zellij-utils/src/plugin_api/resize.proto create mode 100644 zellij-utils/src/plugin_api/resize.rs create mode 100644 zellij-utils/src/plugin_api/style.proto create mode 100644 zellij-utils/src/plugin_api/style.rs diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 76e38d3cf0..2d84232608 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -30,6 +30,10 @@ jobs: options: -v ${{ github.workspace }}/target:/usr/src/zellij --name ssh steps: - uses: actions/checkout@v3 + - name: Install Protoc + uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Add WASM target run: rustup target add wasm32-wasi - name: Install musl-tools diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 315968f620..8f8537dade 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,6 +48,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 + - name: Install Protoc + uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Rust uses: actions-rs/toolchain@v1 with: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index cc0ee2ab9e..19b95fd7a5 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,6 +22,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Install Protoc + uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup toolchain run: rustup show - uses: Swatinem/rust-cache@v2 @@ -40,6 +45,10 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Install Protoc + uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Setup toolchain run: rustup show - uses: Swatinem/rust-cache@v2 @@ -63,6 +72,10 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Install Protoc + uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Setup toolchain run: rustup show - uses: Swatinem/rust-cache@v2 diff --git a/Cargo.lock b/Cargo.lock index 83d566361a..e83fe460fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + [[package]] name = "cache-padded" version = "1.2.0" @@ -1507,6 +1513,15 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.2" @@ -1852,6 +1867,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "names" version = "0.14.0" @@ -2168,6 +2189,16 @@ dependencies = [ "sha-1", ] +[[package]] +name = "petgraph" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "phf" version = "0.8.0" @@ -2313,6 +2344,16 @@ dependencies = [ "getopts", ] +[[package]] +name = "prettyplease" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86" +dependencies = [ + "proc-macro2", + "syn 1.0.96", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2352,6 +2393,60 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +dependencies = [ + "bytes", + "heck 0.4.0", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 1.0.96", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.96", +] + +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -4535,6 +4630,8 @@ dependencies = [ "notify-debouncer-full", "once_cell", "percent-encoding", + "prost", + "prost-build", "regex", "rmp-serde", "serde", diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 074aa1a4d0..fe8b034b90 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -22,13 +22,14 @@ impl<'de> ZellijWorker<'de> for TestWorker { fn on_message(&mut self, message: String, payload: String) { if message == "ping" { self.number_of_messages_received += 1; - post_message_to_plugin( - "pong".into(), - format!( + post_message_to_plugin(PluginMessage { + worker_name: None, + name: "pong".into(), + payload: format!( "{}, received {} messages", payload, self.number_of_messages_received ), - ); + }); } } } @@ -143,22 +144,30 @@ impl ZellijPlugin for State { start_or_reload_plugin(plugin_url) }, Key::Ctrl('g') => { - open_file(std::path::PathBuf::from("/path/to/my/file.rs").as_path()); + open_file(FileToOpen { + path: std::path::PathBuf::from("/path/to/my/file.rs"), + ..Default::default() + }); }, Key::Ctrl('h') => { - open_file_floating(std::path::PathBuf::from("/path/to/my/file.rs").as_path()); + open_file_floating(FileToOpen { + path: std::path::PathBuf::from("/path/to/my/file.rs"), + ..Default::default() + }); }, Key::Ctrl('i') => { - open_file_with_line( - std::path::PathBuf::from("/path/to/my/file.rs").as_path(), - 42, - ); + open_file(FileToOpen { + path: std::path::PathBuf::from("/path/to/my/file.rs"), + line_number: Some(42), + ..Default::default() + }); }, Key::Ctrl('j') => { - open_file_with_line_floating( - std::path::PathBuf::from("/path/to/my/file.rs").as_path(), - 42, - ); + open_file_floating(FileToOpen { + path: std::path::PathBuf::from("/path/to/my/file.rs"), + line_number: Some(42), + ..Default::default() + }); }, Key::Ctrl('k') => { open_terminal(std::path::PathBuf::from("/path/to/my/file.rs").as_path()); @@ -169,16 +178,18 @@ impl ZellijPlugin for State { ); }, Key::Ctrl('m') => { - open_command_pane( - std::path::PathBuf::from("/path/to/my/file.rs").as_path(), - vec!["arg1".to_owned(), "arg2".to_owned()], - ); + open_command_pane(CommandToRun { + path: std::path::PathBuf::from("/path/to/my/file.rs"), + args: vec!["arg1".to_owned(), "arg2".to_owned()], + ..Default::default() + }); }, Key::Ctrl('n') => { - open_command_pane_floating( - std::path::PathBuf::from("/path/to/my/file.rs").as_path(), - vec!["arg1".to_owned(), "arg2".to_owned()], - ); + open_command_pane_floating(CommandToRun { + path: std::path::PathBuf::from("/path/to/my/file.rs"), + args: vec!["arg1".to_owned(), "arg2".to_owned()], + ..Default::default() + }); }, Key::Ctrl('o') => { switch_tab_to(1); @@ -225,7 +236,11 @@ impl ZellijPlugin for State { }, Event::SystemClipboardFailure => { // this is just to trigger the worker message - post_message_to("test", "ping", "gimme_back_my_payload"); + post_message_to(PluginMessage { + worker_name: Some("test".into()), + name: "ping".into(), + payload: "gimme_back_my_payload".into(), + }); }, _ => {}, } diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs index 35ff92ea77..e620930c05 100644 --- a/default-plugins/strider/src/main.rs +++ b/default-plugins/strider/src/main.rs @@ -31,16 +31,16 @@ impl ZellijPlugin for State { EventType::FileSystemUpdate, EventType::FileSystemDelete, ]); - post_message_to( - "file_name_search", - &serde_json::to_string(&MessageToSearch::ScanFolder).unwrap(), - "", - ); - post_message_to( - "file_contents_search", - &serde_json::to_string(&MessageToSearch::ScanFolder).unwrap(), - "", - ); + post_message_to(PluginMessage { + worker_name: Some("file_name_search".into()), + name: serde_json::to_string(&MessageToSearch::ScanFolder).unwrap(), + payload: "".into(), + }); + post_message_to(PluginMessage { + worker_name: Some("file_contents_search".into()), + name: serde_json::to_string(&MessageToSearch::ScanFolder).unwrap(), + payload: "".into(), + }); self.search_state.loading = true; set_timeout(0.5); // for displaying loading animation } @@ -191,48 +191,48 @@ impl ZellijPlugin for State { .iter() .map(|p| p.to_string_lossy().to_string()) .collect(); - post_message_to( - "file_name_search", - &serde_json::to_string(&MessageToSearch::FileSystemCreate).unwrap(), - &serde_json::to_string(&paths).unwrap(), - ); - post_message_to( - "file_contents_search", - &serde_json::to_string(&MessageToSearch::FileSystemCreate).unwrap(), - &serde_json::to_string(&paths).unwrap(), - ); + post_message_to(PluginMessage { + worker_name: Some("file_name_search".into()), + name: serde_json::to_string(&MessageToSearch::FileSystemCreate).unwrap(), + payload: serde_json::to_string(&paths).unwrap(), + }); + post_message_to(PluginMessage { + worker_name: Some("file_contents_search".into()), + name: serde_json::to_string(&MessageToSearch::FileSystemCreate).unwrap(), + payload: serde_json::to_string(&paths).unwrap(), + }); }, Event::FileSystemUpdate(paths) => { let paths: Vec = paths .iter() .map(|p| p.to_string_lossy().to_string()) .collect(); - post_message_to( - "file_name_search", - &serde_json::to_string(&MessageToSearch::FileSystemUpdate).unwrap(), - &serde_json::to_string(&paths).unwrap(), - ); - post_message_to( - "file_contents_search", - &serde_json::to_string(&MessageToSearch::FileSystemUpdate).unwrap(), - &serde_json::to_string(&paths).unwrap(), - ); + post_message_to(PluginMessage { + worker_name: Some("file_name_search".into()), + name: serde_json::to_string(&MessageToSearch::FileSystemUpdate).unwrap(), + payload: serde_json::to_string(&paths).unwrap(), + }); + post_message_to(PluginMessage { + worker_name: Some("file_contents_search".into()), + name: serde_json::to_string(&MessageToSearch::FileSystemUpdate).unwrap(), + payload: serde_json::to_string(&paths).unwrap(), + }); }, Event::FileSystemDelete(paths) => { let paths: Vec = paths .iter() .map(|p| p.to_string_lossy().to_string()) .collect(); - post_message_to( - "file_name_search", - &serde_json::to_string(&MessageToSearch::FileSystemDelete).unwrap(), - &serde_json::to_string(&paths).unwrap(), - ); - post_message_to( - "file_contents_search", - &serde_json::to_string(&MessageToSearch::FileSystemDelete).unwrap(), - &serde_json::to_string(&paths).unwrap(), - ); + post_message_to(PluginMessage { + worker_name: Some("file_name_search".into()), + name: serde_json::to_string(&MessageToSearch::FileSystemDelete).unwrap(), + payload: serde_json::to_string(&paths).unwrap(), + }); + post_message_to(PluginMessage { + worker_name: Some("file_contents_search".into()), + name: serde_json::to_string(&MessageToSearch::FileSystemDelete).unwrap(), + payload: serde_json::to_string(&paths).unwrap(), + }); }, _ => { dbg!("Unknown event {:?}", event); diff --git a/default-plugins/strider/src/search/mod.rs b/default-plugins/strider/src/search/mod.rs index 33c8a60b06..6ffa60cba5 100644 --- a/default-plugins/strider/src/search/mod.rs +++ b/default-plugins/strider/src/search/mod.rs @@ -41,10 +41,11 @@ impl Search { match serde_json::from_str::(&message) { Ok(MessageToSearch::ScanFolder) => { self.scan_hd(); - post_message_to_plugin( - serde_json::to_string(&MessageToPlugin::DoneScanningFolder).unwrap(), - "".to_owned(), - ); + post_message_to_plugin(PluginMessage { + worker_name: None, + name: serde_json::to_string(&MessageToPlugin::DoneScanningFolder).unwrap(), + payload: "".to_owned(), + }); }, Ok(MessageToSearch::Search) => { if let Some(current_search_term) = self.read_search_term_from_hd_cache() { @@ -115,16 +116,19 @@ impl Search { } } if let Some(file_names_search_results) = file_names_search_results { - post_message_to_plugin( - serde_json::to_string(&MessageToPlugin::UpdateFileNameSearchResults).unwrap(), - serde_json::to_string(&file_names_search_results).unwrap(), - ); + post_message_to_plugin(PluginMessage { + name: serde_json::to_string(&MessageToPlugin::UpdateFileNameSearchResults).unwrap(), + payload: serde_json::to_string(&file_names_search_results).unwrap(), + ..Default::default() + }); } if let Some(file_contents_search_results) = file_contents_search_results { - post_message_to_plugin( - serde_json::to_string(&MessageToPlugin::UpdateFileContentsSearchResults).unwrap(), - serde_json::to_string(&file_contents_search_results).unwrap(), - ); + post_message_to_plugin(PluginMessage { + name: serde_json::to_string(&MessageToPlugin::UpdateFileContentsSearchResults) + .unwrap(), + payload: serde_json::to_string(&file_contents_search_results).unwrap(), + ..Default::default() + }); } } pub fn rescan_files(&mut self, paths: String) { diff --git a/default-plugins/strider/src/search/search_state.rs b/default-plugins/strider/src/search/search_state.rs index 865a98e553..834a74fe65 100644 --- a/default-plugins/strider/src/search/search_state.rs +++ b/default-plugins/strider/src/search/search_state.rs @@ -3,8 +3,8 @@ use crate::search::{MessageToSearch, ResultsOfSearch}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use zellij_tile::prelude::{ - hide_self, open_file, open_file_floating, open_file_with_line, open_file_with_line_floating, - open_terminal, open_terminal_floating, post_message_to, Key, + hide_self, open_file, open_file_floating, open_terminal, open_terminal_floating, + post_message_to, FileToOpen, Key, PluginMessage, }; pub const CURRENT_SEARCH_TERM: &str = "/data/current_search_term"; @@ -88,18 +88,32 @@ impl SearchState { match self.selected_search_result_entry() { Some(SearchResult::File { path, .. }) => { if self.should_open_floating { - open_file_floating(&PathBuf::from(path)) + open_file_floating(FileToOpen { + path: PathBuf::from(path), + ..Default::default() + }); } else { - open_file(&PathBuf::from(path)); + open_file(FileToOpen { + path: PathBuf::from(path), + ..Default::default() + }); } }, Some(SearchResult::LineInFile { path, line_number, .. }) => { if self.should_open_floating { - open_file_with_line_floating(&PathBuf::from(path), line_number); + open_file_floating(FileToOpen { + path: PathBuf::from(path), + line_number: Some(line_number), + ..Default::default() + }); } else { - open_file_with_line(&PathBuf::from(path), line_number); + open_file(FileToOpen { + path: PathBuf::from(path), + line_number: Some(line_number), + ..Default::default() + }); } }, None => eprintln!("Search results not found"), @@ -153,16 +167,16 @@ impl SearchState { match std::fs::write(CURRENT_SEARCH_TERM, &self.search_term) { Ok(_) => { if !self.search_term.is_empty() { - post_message_to( - "file_name_search", - &serde_json::to_string(&MessageToSearch::Search).unwrap(), - "", - ); - post_message_to( - "file_contents_search", - &serde_json::to_string(&MessageToSearch::Search).unwrap(), - "", - ); + post_message_to(PluginMessage { + worker_name: Some("file_name_search".into()), + name: serde_json::to_string(&MessageToSearch::Search).unwrap(), + payload: "".into(), + }); + post_message_to(PluginMessage { + worker_name: Some("file_contents_search".into()), + name: serde_json::to_string(&MessageToSearch::Search).unwrap(), + payload: "".into(), + }); self.file_name_search_results.clear(); self.file_contents_search_results.clear(); } diff --git a/default-plugins/strider/src/state.rs b/default-plugins/strider/src/state.rs index 8dd09653a5..67e9b89175 100644 --- a/default-plugins/strider/src/state.rs +++ b/default-plugins/strider/src/state.rs @@ -66,7 +66,10 @@ impl State { self.path = p; refresh_directory(self); }, - FsEntry::File(p, _) => open_file(p.strip_prefix(ROOT).unwrap()), + FsEntry::File(p, _) => open_file(FileToOpen { + path: p.strip_prefix(ROOT).unwrap().into(), + ..Default::default() + }), } } } diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs index 73957cf7cc..92a4c04807 100644 --- a/zellij-server/src/plugins/plugin_loader.rs +++ b/zellij-server/src/plugins/plugin_loader.rs @@ -1,27 +1,28 @@ use crate::plugins::plugin_map::{PluginEnv, PluginMap, RunningPlugin, Subscriptions}; use crate::plugins::plugin_worker::{plugin_worker, RunningWorker}; -use crate::plugins::zellij_exports::{wasi_read_string, wasi_write_object, zellij_exports}; +use crate::plugins::zellij_exports::{wasi_write_object, zellij_exports}; use crate::plugins::PluginId; use highway::{HighwayHash, PortableHash}; use log::info; -use semver::Version; use std::{ collections::{HashMap, HashSet}, - fmt, fs, + fs, path::PathBuf, sync::{Arc, Mutex}, }; use url::Url; use wasmer::{ChainableNamedResolver, Instance, Module, Store}; use wasmer_wasi::{Pipe, WasiState}; +use zellij_utils::prost::Message; use crate::{ logging_pipe::LoggingPipe, screen::ScreenInstruction, thread_bus::ThreadSenders, ui::loading_indication::LoadingIndication, ClientId, }; +use zellij_utils::plugin_api::action::ProtobufPluginConfiguration; use zellij_utils::{ - consts::{VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_SESSION_CACHE_DIR, ZELLIJ_TMP_DIR}, + consts::{ZELLIJ_CACHE_DIR, ZELLIJ_SESSION_CACHE_DIR, ZELLIJ_TMP_DIR}, data::PluginCapabilities, errors::prelude::*, input::command::TerminalAction, @@ -43,116 +44,6 @@ macro_rules! display_loading_stage { }}; } -/// Custom error for plugin version mismatch. -/// -/// This is thrown when, during starting a plugin, it is detected that the plugin version doesn't -/// match the zellij version. This is treated as a fatal error and leads to instantaneous -/// termination. -#[derive(Debug)] -pub struct VersionMismatchError { - zellij_version: String, - plugin_version: String, - plugin_path: PathBuf, - // true for builtin plugins - builtin: bool, -} - -impl std::error::Error for VersionMismatchError {} - -impl VersionMismatchError { - pub fn new( - zellij_version: &str, - plugin_version: &str, - plugin_path: &PathBuf, - builtin: bool, - ) -> Self { - VersionMismatchError { - zellij_version: zellij_version.to_owned(), - plugin_version: plugin_version.to_owned(), - plugin_path: plugin_path.to_owned(), - builtin, - } - } -} - -impl fmt::Display for VersionMismatchError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let first_line = if self.builtin { - "It seems your version of zellij was built with outdated core plugins." - } else { - "If you're seeing this error a plugin version doesn't match the current -zellij version." - }; - - write!( - f, - "{} -Detected versions: - -- Plugin version: {} -- Zellij version: {} -- Offending plugin: {} - -If you're a user: - Please contact the distributor of your zellij version and report this error - to them. - -If you're a developer: - Please run zellij with updated plugins. The easiest way to achieve this - is to build zellij with `cargo xtask install`. Also refer to the docs: - https://github.com/zellij-org/zellij/blob/main/CONTRIBUTING.md#building -", - first_line, - self.plugin_version.trim_end(), - self.zellij_version.trim_end(), - self.plugin_path.display() - ) - } -} - -// Returns `Ok` if the plugin version matches the zellij version. -// Returns an `Err` otherwise. -fn assert_plugin_version(instance: &Instance, plugin_env: &PluginEnv) -> Result<()> { - let err_context = || { - format!( - "failed to determine plugin version for plugin {}", - plugin_env.plugin.path.display() - ) - }; - - let plugin_version_func = match instance.exports.get_function("plugin_version") { - Ok(val) => val, - Err(_) => { - return Err(anyError::new(VersionMismatchError::new( - VERSION, - "Unavailable", - &plugin_env.plugin.path, - plugin_env.plugin.is_builtin(), - ))) - }, - }; - - let plugin_version = plugin_version_func - .call(&[]) - .map_err(anyError::new) - .and_then(|_| wasi_read_string(&plugin_env.wasi_env)) - .and_then(|string| Version::parse(&string).context("failed to parse plugin version")) - .with_context(err_context)?; - let zellij_version = Version::parse(VERSION) - .context("failed to parse zellij version") - .with_context(err_context)?; - if plugin_version != zellij_version { - return Err(anyError::new(VersionMismatchError::new( - VERSION, - &plugin_version.to_string(), - &plugin_env.plugin.path, - plugin_env.plugin.is_builtin(), - ))); - } - - Ok(()) -} - pub struct PluginLoader<'a> { plugin_cache: Arc>>, plugin_path: PathBuf, @@ -645,10 +536,8 @@ impl<'a> PluginLoader<'a> { &mut self, module: Module, ) -> Result<(Instance, PluginEnv, Arc>)> { - let err_context = || format!("Failed to create environment for plugin"); let (instance, plugin_env, subscriptions) = self.create_plugin_instance_env_and_subscriptions(&module)?; - assert_plugin_version(&instance, &plugin_env).with_context(err_context)?; // Only do an insert when everything went well! let cloned_plugin = self.plugin.clone(); self.plugin_cache @@ -724,9 +613,17 @@ impl<'a> PluginLoader<'a> { } start_function.call(&[]).with_context(err_context)?; + let protobuf_plugin_configuration: ProtobufPluginConfiguration = self + .plugin + .userspace_configuration + .clone() + .try_into() + .map_err(|e| anyhow!("Failed to serialize user configuration: {:?}", e))?; + let protobuf_bytes = protobuf_plugin_configuration.encode_to_vec(); wasi_write_object( &plugin_env.wasi_env, - &self.plugin.userspace_configuration.inner(), + &protobuf_bytes, + // &self.plugin.userspace_configuration.inner(), ) .with_context(err_context)?; load_function.call(&[]).with_context(err_context)?; diff --git a/zellij-server/src/plugins/plugin_worker.rs b/zellij-server/src/plugins/plugin_worker.rs index bc7303c7c1..9aae0bab81 100644 --- a/zellij-server/src/plugins/plugin_worker.rs +++ b/zellij-server/src/plugins/plugin_worker.rs @@ -1,4 +1,3 @@ -use crate::plugins::plugin_loader::VersionMismatchError; use crate::plugins::plugin_map::PluginEnv; use crate::plugins::zellij_exports::wasi_write_object; use wasmer::Instance; @@ -6,7 +5,9 @@ use wasmer::Instance; use zellij_utils::async_channel::{unbounded, Receiver, Sender}; use zellij_utils::async_std::task; use zellij_utils::errors::prelude::*; -use zellij_utils::{consts::VERSION, input::plugins::PluginConfig}; +use zellij_utils::input::plugins::PluginConfig; +use zellij_utils::plugin_api::message::ProtobufMessage; +use zellij_utils::prost::Message; pub struct RunningWorker { pub instance: Instance, @@ -31,29 +32,19 @@ impl RunningWorker { } pub fn send_message(&self, message: String, payload: String) -> Result<()> { let err_context = || format!("Failed to send message to worker"); - + let protobuf_message = ProtobufMessage { + name: message, + payload, + ..Default::default() + }; + let protobuf_bytes = protobuf_message.encode_to_vec(); let work_function = self .instance .exports .get_function(&self.name) .with_context(err_context)?; - wasi_write_object(&self.plugin_env.wasi_env, &(message, payload)) - .with_context(err_context)?; - work_function.call(&[]).or_else::(|e| { - match e.downcast::() { - Ok(_) => panic!( - "{}", - anyError::new(VersionMismatchError::new( - VERSION, - "Unavailable", - &self.plugin_config.path, - self.plugin_config.is_builtin(), - )) - ), - Err(e) => Err(e).with_context(err_context), - } - })?; - + wasi_write_object(&self.plugin_env.wasi_env, &protobuf_bytes).with_context(err_context)?; + work_function.call(&[]).with_context(err_context)?; Ok(()) } } diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap index 7934131f4b..a66449c602 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__go_to_tab_name_plugin_command.snap @@ -1,11 +1,11 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 2334 +assertion_line: 2709 expression: "format!(\"{:#?}\", new_tab_event)" --- Some( GoToTabName( - "my tab name\n\r", + "my tab name", ( [], [], diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap index 01cb4b6b4d..cfc94db2ac 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__send_configuration_to_plugins.snap @@ -1,11 +1,11 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 4220 +assertion_line: 4225 expression: "format!(\"{:#?}\", go_to_tab_event)" --- Some( GoToTabName( - "{\"fake_config_key_1\": \"fake_config_value_1\", \"fake_config_key_2\": \"fake_config_value_2\"}\n\r", + "{\"fake_config_key_1\": \"fake_config_value_1\", \"fake_config_key_2\": \"fake_config_value_2\"}", ( [], [], diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap index 504134d01f..8c881def65 100644 --- a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__write_chars_plugin_command.snap @@ -1,6 +1,6 @@ --- source: zellij-server/src/plugins/./unit/plugin_tests.rs -assertion_line: 1171 +assertion_line: 1449 expression: "format!(\"{:#?}\", new_tab_event)" --- Some( @@ -9,8 +9,6 @@ Some( 102, 111, 111, - 10, - 13, ], 1, ), diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs index 5ae23be4f2..d6fcf1fc6e 100644 --- a/zellij-server/src/plugins/wasm_bridge.rs +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -1,5 +1,5 @@ use super::{PluginId, PluginInstruction}; -use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError}; +use crate::plugins::plugin_loader::PluginLoader; use crate::plugins::plugin_map::{AtomicEvent, PluginEnv, PluginMap, RunningPlugin, Subscriptions}; use crate::plugins::plugin_worker::MessageToWorker; use crate::plugins::watch_filesystem::watch_filesystem; @@ -14,13 +14,15 @@ use std::{ use wasmer::{Instance, Module, Store, Value}; use zellij_utils::async_std::task::{self, JoinHandle}; use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap}; +use zellij_utils::plugin_api::event::ProtobufEvent; + +use zellij_utils::prost::Message; use crate::{ background_jobs::BackgroundJob, screen::ScreenInstruction, thread_bus::ThreadSenders, ui::loading_indication::LoadingIndication, ClientId, }; use zellij_utils::{ - consts::VERSION, data::{Event, EventType, PluginCapabilities}, errors::prelude::*, input::{ @@ -737,26 +739,17 @@ pub fn apply_event_to_plugin( plugin_bytes: &mut Vec<(PluginId, ClientId, Vec)>, ) -> Result<()> { let err_context = || format!("Failed to apply event to plugin {plugin_id}"); + let protobuf_event: ProtobufEvent = event + .clone() + .try_into() + .map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?; let update = instance .exports .get_function("update") .with_context(err_context)?; - wasi_write_object(&plugin_env.wasi_env, &event).with_context(err_context)?; - let update_return = - update - .call(&[]) - .or_else::(|e| match e.downcast::() { - Ok(_) => panic!( - "{}", - anyError::new(VersionMismatchError::new( - VERSION, - "Unavailable", - &plugin_env.plugin.path, - plugin_env.plugin.is_builtin(), - )) - ), - Err(e) => Err(e).with_context(err_context), - })?; + wasi_write_object(&plugin_env.wasi_env, &protobuf_event.encode_to_vec()) + .with_context(err_context)?; + let update_return = update.call(&[]).with_context(err_context)?; let should_render = match update_return.get(0) { Some(Value::I32(n)) => *n == 1, _ => false, diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index c73068470a..275de13e6d 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -3,7 +3,7 @@ use crate::plugins::plugin_map::{PluginEnv, Subscriptions}; use crate::plugins::wasm_bridge::handle_plugin_crash; use crate::route::route_action; use log::{debug, warn}; -use serde::{de::DeserializeOwned, Serialize}; +use serde::Serialize; use std::{ collections::{BTreeMap, HashSet}, path::PathBuf, @@ -21,7 +21,10 @@ use crate::{panes::PaneId, screen::ScreenInstruction}; use zellij_utils::{ consts::VERSION, - data::{Direction, Event, EventType, InputMode, PluginIds, Resize}, + data::{ + CommandToRun, Direction, Event, EventType, FileToOpen, InputMode, PluginCommand, PluginIds, + PluginMessage, Resize, ResizeStrategy, + }, errors::prelude::*, input::{ actions::Action, @@ -29,6 +32,11 @@ use zellij_utils::{ layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation}, plugins::PluginType, }, + plugin_api::{ + plugin_command::ProtobufPluginCommand, + plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion}, + }, + prost::Message, serde, }; @@ -53,87 +61,13 @@ pub fn zellij_exports( plugin_env: &PluginEnv, subscriptions: &Arc>, ) -> ImportObject { - macro_rules! zellij_export { - ($($host_function:ident),+ $(,)?) => { - imports! { - "zellij" => { - $(stringify!($host_function) => - Function::new_native_with_env(store, ForeignFunctionEnv::new(plugin_env, subscriptions), $host_function),)+ - } - } + imports! { + "zellij" => { + "host_run_plugin_command" => { + Function::new_native_with_env(store, ForeignFunctionEnv::new(plugin_env, subscriptions), host_run_plugin_command) + } } } - - zellij_export! { - host_subscribe, - host_unsubscribe, - host_set_selectable, - host_get_plugin_ids, - host_get_zellij_version, - host_open_file, - host_open_file_floating, - host_open_file_with_line, - host_open_file_with_line_floating, - host_open_terminal, - host_open_terminal_floating, - host_open_command_pane, - host_open_command_pane_floating, - host_switch_tab_to, - host_set_timeout, - host_exec_cmd, - host_report_panic, - host_post_message_to, - host_post_message_to_plugin, - host_hide_self, - host_show_self, - host_switch_to_mode, - host_new_tabs_with_layout, - host_new_tab, - host_go_to_next_tab, - host_go_to_previous_tab, - host_resize, - host_resize_with_direction, - host_focus_next_pane, - host_focus_previous_pane, - host_move_focus, - host_move_focus_or_tab, - host_detach, - host_edit_scrollback, - host_write, - host_write_chars, - host_toggle_tab, - host_move_pane, - host_move_pane_with_direction, - host_clear_screen, - host_scroll_up, - host_scroll_down, - host_scroll_to_top, - host_scroll_to_bottom, - host_page_scroll_up, - host_page_scroll_down, - host_toggle_focus_fullscreen, - host_toggle_pane_frames, - host_toggle_pane_embed_or_eject, - host_undo_rename_pane, - host_close_focus, - host_toggle_active_tab_sync, - host_close_focused_tab, - host_undo_rename_tab, - host_quit_zellij, - host_previous_swap_layout, - host_next_swap_layout, - host_go_to_tab_name, - host_focus_or_create_tab, - host_go_to_tab, - host_start_or_reload_plugin, - host_close_terminal_pane, - host_close_plugin_pane, - host_focus_terminal_pane, - host_focus_plugin_pane, - host_rename_terminal_pane, - host_rename_plugin_pane, - host_rename_tab, - } } #[derive(WasmerEnv, Clone)] @@ -151,42 +85,151 @@ impl ForeignFunctionEnv { } } -fn host_subscribe(env: &ForeignFunctionEnv) { - wasi_read_object::>(&env.plugin_env.wasi_env) - .and_then(|new| { - env.subscriptions.lock().to_anyhow()?.extend(new.clone()); - Ok(new) - }) - .and_then(|new| { - env.plugin_env - .senders - .send_to_plugin(PluginInstruction::PluginSubscribedToEvents( - env.plugin_env.plugin_id, - env.plugin_env.client_id, - new, - )) +fn host_run_plugin_command(env: &ForeignFunctionEnv) { + wasi_read_bytes(&env.plugin_env.wasi_env) + .and_then(|bytes| { + let command: ProtobufPluginCommand = ProtobufPluginCommand::decode(bytes.as_slice())?; + let command: PluginCommand = command + .try_into() + .map_err(|e| anyhow!("failed to convert serialized command: {}", e))?; + match command { + PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?, + PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?, + PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable), + PluginCommand::GetPluginIds => get_plugin_ids(env), + PluginCommand::GetZellijVersion => get_zellij_version(env), + PluginCommand::OpenFile(file_to_open) => open_file(env, file_to_open), + PluginCommand::OpenFileFloating(file_to_open) => { + open_file_floating(env, file_to_open) + }, + PluginCommand::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?), + PluginCommand::OpenTerminalFloating(cwd) => { + open_terminal_floating(env, cwd.path.try_into()?) + }, + PluginCommand::OpenCommandPane(command_to_run) => { + open_command_pane(env, command_to_run) + }, + PluginCommand::OpenCommandPaneFloating(command_to_run) => { + open_command_pane_floating(env, command_to_run) + }, + PluginCommand::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index), + PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds), + PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line), + PluginCommand::PostMessageTo(plugin_message) => { + post_message_to(env, plugin_message)? + }, + PluginCommand::PostMessageToPlugin(plugin_message) => { + post_message_to_plugin(env, plugin_message)? + }, + PluginCommand::HideSelf => hide_self(env)?, + PluginCommand::ShowSelf(should_float_if_hidden) => { + show_self(env, should_float_if_hidden) + }, + PluginCommand::SwitchToMode(input_mode) => { + switch_to_mode(env, input_mode.try_into()?) + }, + PluginCommand::NewTabsWithLayout(raw_layout) => { + new_tabs_with_layout(env, &raw_layout)? + }, + PluginCommand::NewTab => new_tab(env), + PluginCommand::GoToNextTab => go_to_next_tab(env), + PluginCommand::GoToPreviousTab => go_to_previous_tab(env), + PluginCommand::Resize(resize_payload) => resize(env, resize_payload), + PluginCommand::ResizeWithDirection(resize_strategy) => { + resize_with_direction(env, resize_strategy) + }, + PluginCommand::FocusNextPane => focus_next_pane(env), + PluginCommand::FocusPreviousPane => focus_previous_pane(env), + PluginCommand::MoveFocus(direction) => move_focus(env, direction), + PluginCommand::MoveFocusOrTab(direction) => move_focus_or_tab(env, direction), + PluginCommand::Detach => detach(env), + PluginCommand::EditScrollback => edit_scrollback(env), + PluginCommand::Write(bytes) => write(env, bytes), + PluginCommand::WriteChars(chars) => write_chars(env, chars), + PluginCommand::ToggleTab => toggle_tab(env), + PluginCommand::MovePane => move_pane(env), + PluginCommand::MovePaneWithDirection(direction) => { + move_pane_with_direction(env, direction) + }, + PluginCommand::ClearScreen => clear_screen(env), + PluginCommand::ScrollUp => scroll_up(env), + PluginCommand::ScrollDown => scroll_down(env), + PluginCommand::ScrollToTop => scroll_to_top(env), + PluginCommand::ScrollToBottom => scroll_to_bottom(env), + PluginCommand::PageScrollUp => page_scroll_up(env), + PluginCommand::PageScrollDown => page_scroll_down(env), + PluginCommand::ToggleFocusFullscreen => toggle_focus_fullscreen(env), + PluginCommand::TogglePaneFrames => toggle_pane_frames(env), + PluginCommand::TogglePaneEmbedOrEject => toggle_pane_embed_or_eject(env), + PluginCommand::UndoRenamePane => undo_rename_pane(env), + PluginCommand::CloseFocus => close_focus(env), + PluginCommand::ToggleActiveTabSync => toggle_active_tab_sync(env), + PluginCommand::CloseFocusedTab => close_focused_tab(env), + PluginCommand::UndoRenameTab => undo_rename_tab(env), + PluginCommand::QuitZellij => quit_zellij(env), + PluginCommand::PreviousSwapLayout => previous_swap_layout(env), + PluginCommand::NextSwapLayout => next_swap_layout(env), + PluginCommand::GoToTabName(tab_name) => go_to_tab_name(env, tab_name), + PluginCommand::FocusOrCreateTab(tab_name) => focus_or_create_tab(env, tab_name), + PluginCommand::GoToTab(tab_index) => go_to_tab(env, tab_index), + PluginCommand::StartOrReloadPlugin(plugin_url) => { + start_or_reload_plugin(env, &plugin_url)? + }, + PluginCommand::CloseTerminalPane(terminal_pane_id) => { + close_terminal_pane(env, terminal_pane_id) + }, + PluginCommand::ClosePluginPane(plugin_pane_id) => { + close_plugin_pane(env, plugin_pane_id) + }, + PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden) => { + focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden) + }, + PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => { + focus_plugin_pane(env, plugin_pane_id, should_float_if_hidden) + }, + PluginCommand::RenameTerminalPane(terminal_pane_id, new_name) => { + rename_terminal_pane(env, terminal_pane_id, &new_name) + }, + PluginCommand::RenamePluginPane(plugin_pane_id, new_name) => { + rename_plugin_pane(env, plugin_pane_id, &new_name) + }, + PluginCommand::RenameTab(tab_index, new_name) => { + rename_tab(env, tab_index, &new_name) + }, + PluginCommand::ReportPanic(crash_payload) => report_panic(env, &crash_payload), + } + Ok(()) }) - .with_context(|| format!("failed to subscribe for plugin {}", env.plugin_env.name())) - .fatal(); + .with_context(|| format!("failed to run plugin command {}", env.plugin_env.name())) + .non_fatal(); } -fn host_unsubscribe(env: &ForeignFunctionEnv) { - wasi_read_object::>(&env.plugin_env.wasi_env) - .and_then(|old| { - env.subscriptions - .lock() - .to_anyhow()? - .retain(|k| !old.contains(k)); - Ok(()) - }) - .with_context(|| format!("failed to unsubscribe for plugin {}", env.plugin_env.name())) - .fatal(); +fn subscribe(env: &ForeignFunctionEnv, event_list: HashSet) -> Result<()> { + env.subscriptions + .lock() + .to_anyhow()? + .extend(event_list.clone()); + env.plugin_env + .senders + .send_to_plugin(PluginInstruction::PluginSubscribedToEvents( + env.plugin_env.plugin_id, + env.plugin_env.client_id, + event_list, + )) } -fn host_set_selectable(env: &ForeignFunctionEnv, selectable: i32) { +fn unsubscribe(env: &ForeignFunctionEnv, event_list: HashSet) -> Result<()> { + env.subscriptions + .lock() + .to_anyhow()? + .retain(|k| !event_list.contains(k)); + Ok(()) +} + +fn set_selectable(env: &ForeignFunctionEnv, selectable: bool) { match env.plugin_env.plugin.run { PluginType::Pane(Some(tab_index)) => { - let selectable = selectable != 0; + // let selectable = selectable != 0; env.plugin_env .senders .send_to_screen(ScreenInstruction::SetSelectable( @@ -205,221 +248,151 @@ fn host_set_selectable(env: &ForeignFunctionEnv, selectable: i32) { }, _ => { debug!( - "{} - Calling method 'host_set_selectable' does nothing for headless plugins", + "{} - Calling method 'set_selectable' does nothing for headless plugins", env.plugin_env.plugin.location ) }, } } -fn host_get_plugin_ids(env: &ForeignFunctionEnv) { +fn get_plugin_ids(env: &ForeignFunctionEnv) { let ids = PluginIds { plugin_id: env.plugin_env.plugin_id, zellij_pid: process::id(), }; - wasi_write_object(&env.plugin_env.wasi_env, &ids) - .with_context(|| { - format!( - "failed to query plugin IDs from host for plugin {}", - env.plugin_env.name() - ) - }) - .non_fatal(); -} - -fn host_get_zellij_version(env: &ForeignFunctionEnv) { - wasi_write_object(&env.plugin_env.wasi_env, VERSION) - .with_context(|| { - format!( - "failed to request zellij version from host for plugin {}", - env.plugin_env.name() - ) - }) - .non_fatal(); -} - -fn host_open_file(env: &ForeignFunctionEnv) { - wasi_read_object::(&env.plugin_env.wasi_env) - .and_then(|path| { - let error_msg = || { - format!( - "failed to open floating file in plugin {}", - env.plugin_env.name() - ) - }; - let floating = false; - let action = Action::EditFile(path, None, None, None, floating); // TODO: add cwd - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(|| { - format!( - "failed to open file on host from plugin {}", - env.plugin_env.name() - ) - }) - .non_fatal(); -} - -fn host_open_file_floating(env: &ForeignFunctionEnv) { - wasi_read_object::(&env.plugin_env.wasi_env) - .and_then(|path| { - let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); - let floating = true; - let action = Action::EditFile(path, None, None, None, floating); // TODO: add cwd - apply_action!(action, error_msg, env); + ProtobufPluginIds::try_from(ids) + .map_err(|e| anyhow!("Failed to serialized plugin ids: {}", e)) + .and_then(|serialized| { + wasi_write_object(&env.plugin_env.wasi_env, &serialized.encode_to_vec())?; Ok(()) }) .with_context(|| { format!( - "failed to open file on host from plugin {}", - env.plugin_env.name() - ) - }) - .non_fatal(); -} - -fn host_open_file_with_line(env: &ForeignFunctionEnv) { - wasi_read_object::<(PathBuf, usize)>(&env.plugin_env.wasi_env) - .and_then(|(path, line)| { - let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); - let floating = false; - let action = Action::EditFile(path, Some(line), None, None, floating); // TODO: add cwd - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(|| { - format!( - "failed to open file on host from plugin {}", - env.plugin_env.name() - ) - }) - .non_fatal(); -} - -fn host_open_file_with_line_floating(env: &ForeignFunctionEnv) { - wasi_read_object::<(PathBuf, usize)>(&env.plugin_env.wasi_env) - .and_then(|(path, line)| { - let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); - let floating = true; - let action = Action::EditFile(path, Some(line), None, None, floating); // TODO: add cwd - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(|| { - format!( - "failed to open file on host from plugin {}", + "failed to query plugin IDs from host for plugin {}", env.plugin_env.name() ) }) .non_fatal(); } -fn host_open_terminal(env: &ForeignFunctionEnv) { - wasi_read_object::(&env.plugin_env.wasi_env) - .and_then(|path| { - let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); - let mut default_shell = env - .plugin_env - .default_shell - .clone() - .unwrap_or_else(|| TerminalAction::RunCommand(RunCommand::default())); - default_shell.change_cwd(path); - let run_command_action: Option = match default_shell { - TerminalAction::RunCommand(run_command) => Some(run_command.into()), - _ => None, - }; - let action = Action::NewTiledPane(None, run_command_action, None); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(|| { - format!( - "failed to open file on host from plugin {}", - env.plugin_env.name() - ) - }) - .non_fatal(); +fn get_zellij_version(env: &ForeignFunctionEnv) { + let protobuf_zellij_version = ProtobufZellijVersion { + version: VERSION.to_owned(), + }; + wasi_write_object( + &env.plugin_env.wasi_env, + &protobuf_zellij_version.encode_to_vec(), + ) + .with_context(|| { + format!( + "failed to request zellij version from host for plugin {}", + env.plugin_env.name() + ) + }) + .non_fatal(); +} + +fn open_file(env: &ForeignFunctionEnv, file_to_open: FileToOpen) { + let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); + let floating = false; + let action = Action::EditFile( + file_to_open.path, + file_to_open.line_number, + file_to_open.cwd, + None, + floating, + ); + apply_action!(action, error_msg, env); } -fn host_open_terminal_floating(env: &ForeignFunctionEnv) { - wasi_read_object::(&env.plugin_env.wasi_env) - .and_then(|path| { - let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); - let mut default_shell = env - .plugin_env - .default_shell - .clone() - .unwrap_or_else(|| TerminalAction::RunCommand(RunCommand::default())); - default_shell.change_cwd(path); - let run_command_action: Option = match default_shell { - TerminalAction::RunCommand(run_command) => Some(run_command.into()), - _ => None, - }; - let action = Action::NewFloatingPane(run_command_action, None); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(|| { - format!( - "failed to open file on host from plugin {}", - env.plugin_env.name() - ) - }) - .non_fatal(); +fn open_file_floating(env: &ForeignFunctionEnv, file_to_open: FileToOpen) { + let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); + let floating = true; + let action = Action::EditFile( + file_to_open.path, + file_to_open.line_number, + file_to_open.cwd, + None, + floating, + ); + apply_action!(action, error_msg, env); } -fn host_open_command_pane(env: &ForeignFunctionEnv) { - let error_msg = || format!("failed to run command in plugin {}", env.plugin_env.name()); - wasi_read_object::<(PathBuf, Vec)>(&env.plugin_env.wasi_env) - .and_then(|(command, args)| { - let cwd = None; - let direction = None; - let hold_on_close = true; - let hold_on_start = false; - let name = None; - let run_command_action = RunCommandAction { - command, - args, - cwd, - direction, - hold_on_close, - hold_on_start, - }; - let action = Action::NewTiledPane(direction, Some(run_command_action), name); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .non_fatal(); +fn open_terminal(env: &ForeignFunctionEnv, cwd: PathBuf) { + let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); + let mut default_shell = env + .plugin_env + .default_shell + .clone() + .unwrap_or_else(|| TerminalAction::RunCommand(RunCommand::default())); + default_shell.change_cwd(cwd); + let run_command_action: Option = match default_shell { + TerminalAction::RunCommand(run_command) => Some(run_command.into()), + _ => None, + }; + let action = Action::NewTiledPane(None, run_command_action, None); + apply_action!(action, error_msg, env); } -fn host_open_command_pane_floating(env: &ForeignFunctionEnv) { - let error_msg = || format!("failed to run command in plugin {}", env.plugin_env.name()); - wasi_read_object::<(PathBuf, Vec)>(&env.plugin_env.wasi_env) - .and_then(|(command, args)| { - let cwd = None; - let direction = None; - let hold_on_close = true; - let hold_on_start = false; - let name = None; - let run_command_action = RunCommandAction { - command, - args, - cwd, - direction, - hold_on_close, - hold_on_start, - }; - let action = Action::NewFloatingPane(Some(run_command_action), name); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .non_fatal(); +fn open_terminal_floating(env: &ForeignFunctionEnv, cwd: PathBuf) { + let error_msg = || format!("failed to open file in plugin {}", env.plugin_env.name()); + let mut default_shell = env + .plugin_env + .default_shell + .clone() + .unwrap_or_else(|| TerminalAction::RunCommand(RunCommand::default())); + default_shell.change_cwd(cwd); + let run_command_action: Option = match default_shell { + TerminalAction::RunCommand(run_command) => Some(run_command.into()), + _ => None, + }; + let action = Action::NewFloatingPane(run_command_action, None); + apply_action!(action, error_msg, env); +} + +fn open_command_pane(env: &ForeignFunctionEnv, command_to_run: CommandToRun) { + let error_msg = || format!("failed to open command in plugin {}", env.plugin_env.name()); + let command = command_to_run.path; + let cwd = command_to_run.cwd; + let args = command_to_run.args; + let direction = None; + let hold_on_close = true; + let hold_on_start = false; + let name = None; + let run_command_action = RunCommandAction { + command, + args, + cwd, + direction, + hold_on_close, + hold_on_start, + }; + let action = Action::NewTiledPane(direction, Some(run_command_action), name); + apply_action!(action, error_msg, env); +} + +fn open_command_pane_floating(env: &ForeignFunctionEnv, command_to_run: CommandToRun) { + let error_msg = || format!("failed to open command in plugin {}", env.plugin_env.name()); + let command = command_to_run.path; + let cwd = command_to_run.cwd; + let args = command_to_run.args; + let direction = None; + let hold_on_close = true; + let hold_on_start = false; + let name = None; + let run_command_action = RunCommandAction { + command, + args, + cwd, + direction, + hold_on_close, + hold_on_start, + }; + let action = Action::NewFloatingPane(Some(run_command_action), name); + apply_action!(action, error_msg, env); } -fn host_switch_tab_to(env: &ForeignFunctionEnv, tab_idx: u32) { +fn switch_tab_to(env: &ForeignFunctionEnv, tab_idx: u32) { env.plugin_env .senders .send_to_screen(ScreenInstruction::GoToTab( @@ -428,14 +401,14 @@ fn host_switch_tab_to(env: &ForeignFunctionEnv, tab_idx: u32) { )) .with_context(|| { format!( - "failed to switch host to tab {tab_idx} from plugin {}", + "failed to switch to tab {tab_idx} from plugin {}", env.plugin_env.name() ) }) .non_fatal(); } -fn host_set_timeout(env: &ForeignFunctionEnv, secs: f64) { +fn set_timeout(env: &ForeignFunctionEnv, secs: f32) { // There is a fancy, high-performance way to do this with zero additional threads: // If the plugin thread keeps a BinaryHeap of timer structs, it can manage multiple and easily `.peek()` at the // next time to trigger in O(1) time. Once the wake-up time is known, the `wasm` thread can use `recv_timeout()` @@ -452,7 +425,7 @@ fn host_set_timeout(env: &ForeignFunctionEnv, secs: f64) { // TODO: we should really use an async task for this thread::spawn(move || { let start_time = Instant::now(); - thread::sleep(Duration::from_secs_f64(secs)); + thread::sleep(Duration::from_secs_f32(secs)); // FIXME: The way that elapsed time is being calculated here is not exact; it doesn't take into account the // time it takes an event to actually reach the plugin after it's sent to the `wasm` thread. let elapsed_time = Instant::now().duration_since(start_time).as_secs_f64(); @@ -478,67 +451,62 @@ fn host_set_timeout(env: &ForeignFunctionEnv, secs: f64) { }); } -fn host_exec_cmd(env: &ForeignFunctionEnv) { +fn exec_cmd(env: &ForeignFunctionEnv, mut command_line: Vec) { let err_context = || { format!( "failed to execute command on host for plugin '{}'", env.plugin_env.name() ) }; - - let mut cmdline: Vec = wasi_read_object(&env.plugin_env.wasi_env) - .with_context(err_context) - .fatal(); - let command = cmdline.remove(0); + let command = command_line.remove(0); // Bail out if we're forbidden to run command if !env.plugin_env.plugin._allow_exec_host_cmd { warn!("This plugin isn't allow to run command in host side, skip running this command: '{cmd} {args}'.", - cmd = command, args = cmdline.join(" ")); + cmd = command, args = command_line.join(" ")); return; } // Here, we don't wait the command to finish process::Command::new(command) - .args(cmdline) + .args(command_line) .spawn() .with_context(err_context) .non_fatal(); } -fn host_post_message_to(env: &ForeignFunctionEnv) { - wasi_read_object::<(String, String, String)>(&env.plugin_env.wasi_env) - .and_then(|(worker_name, message, payload)| { - env.plugin_env - .senders - .send_to_plugin(PluginInstruction::PostMessagesToPluginWorker( - env.plugin_env.plugin_id, - env.plugin_env.client_id, - worker_name, - vec![(message, payload)], - )) - }) - .with_context(|| format!("failed to post message to worker {}", env.plugin_env.name())) - .fatal(); +fn post_message_to(env: &ForeignFunctionEnv, plugin_message: PluginMessage) -> Result<()> { + let worker_name = plugin_message + .worker_name + .ok_or(anyhow!("Worker name not specified in message to worker"))?; + env.plugin_env + .senders + .send_to_plugin(PluginInstruction::PostMessagesToPluginWorker( + env.plugin_env.plugin_id, + env.plugin_env.client_id, + worker_name, + vec![(plugin_message.name, plugin_message.payload)], + )) } -fn host_post_message_to_plugin(env: &ForeignFunctionEnv) { - wasi_read_object::<(String, String)>(&env.plugin_env.wasi_env) - .and_then(|(message, payload)| { - env.plugin_env - .senders - .send_to_plugin(PluginInstruction::PostMessageToPlugin( - env.plugin_env.plugin_id, - env.plugin_env.client_id, - message, - payload, - )) - }) - .with_context(|| format!("failed to post message to plugin {}", env.plugin_env.name())) - .fatal(); +fn post_message_to_plugin(env: &ForeignFunctionEnv, plugin_message: PluginMessage) -> Result<()> { + if let Some(worker_name) = plugin_message.worker_name { + return Err(anyhow!( + "Worker name (\"{}\") should not be specified in message to plugin", + worker_name + )); + } + env.plugin_env + .senders + .send_to_plugin(PluginInstruction::PostMessageToPlugin( + env.plugin_env.plugin_id, + env.plugin_env.client_id, + plugin_message.name, + plugin_message.payload, + )) } -fn host_hide_self(env: &ForeignFunctionEnv) { +fn hide_self(env: &ForeignFunctionEnv) -> Result<()> { env.plugin_env .senders .send_to_screen(ScreenInstruction::SuppressPane( @@ -546,261 +514,206 @@ fn host_hide_self(env: &ForeignFunctionEnv) { env.plugin_env.client_id, )) .with_context(|| format!("failed to hide self")) - .fatal(); } -fn host_show_self(env: &ForeignFunctionEnv, should_float_if_hidden: i32) { - let should_float_if_hidden = should_float_if_hidden != 0; +fn show_self(env: &ForeignFunctionEnv, should_float_if_hidden: bool) { let action = Action::FocusPluginPaneWithId(env.plugin_env.plugin_id, should_float_if_hidden); let error_msg = || format!("Failed to show self for plugin"); apply_action!(action, error_msg, env); } -fn host_switch_to_mode(env: &ForeignFunctionEnv) { - wasi_read_object::(&env.plugin_env.wasi_env) - .and_then(|input_mode| { - let action = Action::SwitchToMode(input_mode); - let error_msg = || { - format!( - "failed to switch to mode in plugin {}", - env.plugin_env.name() - ) - }; - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(|| format!("failed to subscribe for plugin {}", env.plugin_env.name())) - .fatal(); -} - -fn host_new_tabs_with_layout(env: &ForeignFunctionEnv) { - wasi_read_string(&env.plugin_env.wasi_env) - .and_then(|raw_layout| { - Layout::from_str( - &raw_layout, - format!("Layout from plugin: {}", env.plugin_env.name()), - None, - None, - ) - .map_err(|e| anyhow!("Failed to parse layout: {:?}", e)) - }) // TODO: cwd? - .and_then(|layout| { - let mut tabs_to_open = vec![]; - let tabs = layout.tabs(); - if tabs.is_empty() { - let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone()); - let swap_floating_layouts = Some(layout.swap_floating_layouts.clone()); - let action = Action::NewTab( - layout.template.as_ref().map(|t| t.0.clone()), - layout.template.map(|t| t.1).unwrap_or_default(), - swap_tiled_layouts, - swap_floating_layouts, - None, - ); - tabs_to_open.push(action); - } else { - for (tab_name, tiled_pane_layout, floating_pane_layout) in layout.tabs() { - let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone()); - let swap_floating_layouts = Some(layout.swap_floating_layouts.clone()); - let action = Action::NewTab( - Some(tiled_pane_layout), - floating_pane_layout, - swap_tiled_layouts, - swap_floating_layouts, - tab_name, - ); - tabs_to_open.push(action); - } - } - for action in tabs_to_open { - let error_msg = || format!("Failed to create layout tab"); - apply_action!(action, error_msg, env); - } - Ok(()) - }) - .non_fatal(); +fn switch_to_mode(env: &ForeignFunctionEnv, input_mode: InputMode) { + let action = Action::SwitchToMode(input_mode); + let error_msg = || { + format!( + "failed to switch to mode in plugin {}", + env.plugin_env.name() + ) + }; + apply_action!(action, error_msg, env); +} + +fn new_tabs_with_layout(env: &ForeignFunctionEnv, raw_layout: &str) -> Result<()> { + // TODO: cwd + let layout = Layout::from_str( + &raw_layout, + format!("Layout from plugin: {}", env.plugin_env.name()), + None, + None, + ) + .map_err(|e| anyhow!("Failed to parse layout: {:?}", e))?; + let mut tabs_to_open = vec![]; + let tabs = layout.tabs(); + if tabs.is_empty() { + let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone()); + let swap_floating_layouts = Some(layout.swap_floating_layouts.clone()); + let action = Action::NewTab( + layout.template.as_ref().map(|t| t.0.clone()), + layout.template.map(|t| t.1).unwrap_or_default(), + swap_tiled_layouts, + swap_floating_layouts, + None, + ); + tabs_to_open.push(action); + } else { + for (tab_name, tiled_pane_layout, floating_pane_layout) in layout.tabs() { + let swap_tiled_layouts = Some(layout.swap_tiled_layouts.clone()); + let swap_floating_layouts = Some(layout.swap_floating_layouts.clone()); + let action = Action::NewTab( + Some(tiled_pane_layout), + floating_pane_layout, + swap_tiled_layouts, + swap_floating_layouts, + tab_name, + ); + tabs_to_open.push(action); + } + } + for action in tabs_to_open { + let error_msg = || format!("Failed to create layout tab"); + apply_action!(action, error_msg, env); + } + Ok(()) } -fn host_new_tab(env: &ForeignFunctionEnv) { +fn new_tab(env: &ForeignFunctionEnv) { let action = Action::NewTab(None, vec![], None, None, None); let error_msg = || format!("Failed to open new tab"); apply_action!(action, error_msg, env); } -fn host_go_to_next_tab(env: &ForeignFunctionEnv) { +fn go_to_next_tab(env: &ForeignFunctionEnv) { let action = Action::GoToNextTab; let error_msg = || format!("Failed to go to next tab"); apply_action!(action, error_msg, env); } -fn host_go_to_previous_tab(env: &ForeignFunctionEnv) { +fn go_to_previous_tab(env: &ForeignFunctionEnv) { let action = Action::GoToPreviousTab; let error_msg = || format!("Failed to go to previous tab"); apply_action!(action, error_msg, env); } -fn host_resize(env: &ForeignFunctionEnv) { +fn resize(env: &ForeignFunctionEnv, resize: Resize) { let error_msg = || format!("failed to resize in plugin {}", env.plugin_env.name()); - wasi_read_object::(&env.plugin_env.wasi_env) - .and_then(|resize| { - let action = Action::Resize(resize, None); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let action = Action::Resize(resize, None); + apply_action!(action, error_msg, env); } -fn host_resize_with_direction(env: &ForeignFunctionEnv) { +fn resize_with_direction(env: &ForeignFunctionEnv, resize: ResizeStrategy) { let error_msg = || format!("failed to resize in plugin {}", env.plugin_env.name()); - wasi_read_object::<(Resize, Direction)>(&env.plugin_env.wasi_env) - .and_then(|(resize, direction)| { - let action = Action::Resize(resize, Some(direction)); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let action = Action::Resize(resize.resize, resize.direction); + apply_action!(action, error_msg, env); } -fn host_focus_next_pane(env: &ForeignFunctionEnv) { +fn focus_next_pane(env: &ForeignFunctionEnv) { let action = Action::FocusNextPane; let error_msg = || format!("Failed to focus next pane"); apply_action!(action, error_msg, env); } -fn host_focus_previous_pane(env: &ForeignFunctionEnv) { +fn focus_previous_pane(env: &ForeignFunctionEnv) { let action = Action::FocusPreviousPane; let error_msg = || format!("Failed to focus previous pane"); apply_action!(action, error_msg, env); } -fn host_move_focus(env: &ForeignFunctionEnv) { +fn move_focus(env: &ForeignFunctionEnv, direction: Direction) { let error_msg = || format!("failed to move focus in plugin {}", env.plugin_env.name()); - wasi_read_object::(&env.plugin_env.wasi_env) - .and_then(|direction| { - let action = Action::MoveFocus(direction); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let action = Action::MoveFocus(direction); + apply_action!(action, error_msg, env); } -fn host_move_focus_or_tab(env: &ForeignFunctionEnv) { +fn move_focus_or_tab(env: &ForeignFunctionEnv, direction: Direction) { let error_msg = || format!("failed to move focus in plugin {}", env.plugin_env.name()); - wasi_read_object::(&env.plugin_env.wasi_env) - .and_then(|direction| { - let action = Action::MoveFocusOrTab(direction); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let action = Action::MoveFocusOrTab(direction); + apply_action!(action, error_msg, env); } -fn host_detach(env: &ForeignFunctionEnv) { +fn detach(env: &ForeignFunctionEnv) { let action = Action::Detach; let error_msg = || format!("Failed to detach"); apply_action!(action, error_msg, env); } -fn host_edit_scrollback(env: &ForeignFunctionEnv) { +fn edit_scrollback(env: &ForeignFunctionEnv) { let action = Action::EditScrollback; let error_msg = || format!("Failed to edit scrollback"); apply_action!(action, error_msg, env); } -fn host_write(env: &ForeignFunctionEnv) { +fn write(env: &ForeignFunctionEnv, bytes: Vec) { let error_msg = || format!("failed to write in plugin {}", env.plugin_env.name()); - wasi_read_object::>(&env.plugin_env.wasi_env) - .and_then(|bytes| { - let action = Action::Write(bytes); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let action = Action::Write(bytes); + apply_action!(action, error_msg, env); } -fn host_write_chars(env: &ForeignFunctionEnv) { +fn write_chars(env: &ForeignFunctionEnv, chars_to_write: String) { let error_msg = || format!("failed to write in plugin {}", env.plugin_env.name()); - wasi_read_string(&env.plugin_env.wasi_env) - .and_then(|chars_to_write| { - let action = Action::WriteChars(chars_to_write); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let action = Action::WriteChars(chars_to_write); + apply_action!(action, error_msg, env); } -fn host_toggle_tab(env: &ForeignFunctionEnv) { - let action = Action::ToggleTab; +fn toggle_tab(env: &ForeignFunctionEnv) { let error_msg = || format!("Failed to toggle tab"); + let action = Action::ToggleTab; apply_action!(action, error_msg, env); } -fn host_move_pane(env: &ForeignFunctionEnv) { +fn move_pane(env: &ForeignFunctionEnv) { let error_msg = || format!("failed to move pane in plugin {}", env.plugin_env.name()); let action = Action::MovePane(None); apply_action!(action, error_msg, env); } -fn host_move_pane_with_direction(env: &ForeignFunctionEnv) { +fn move_pane_with_direction(env: &ForeignFunctionEnv, direction: Direction) { let error_msg = || format!("failed to move pane in plugin {}", env.plugin_env.name()); - wasi_read_object::(&env.plugin_env.wasi_env) - .and_then(|direction| { - let action = Action::MovePane(Some(direction)); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let action = Action::MovePane(Some(direction)); + apply_action!(action, error_msg, env); } -fn host_clear_screen(env: &ForeignFunctionEnv) { +fn clear_screen(env: &ForeignFunctionEnv) { let error_msg = || format!("failed to clear screen in plugin {}", env.plugin_env.name()); let action = Action::ClearScreen; apply_action!(action, error_msg, env); } -fn host_scroll_up(env: &ForeignFunctionEnv) { +fn scroll_up(env: &ForeignFunctionEnv) { let error_msg = || format!("failed to scroll up in plugin {}", env.plugin_env.name()); let action = Action::ScrollUp; apply_action!(action, error_msg, env); } -fn host_scroll_down(env: &ForeignFunctionEnv) { +fn scroll_down(env: &ForeignFunctionEnv) { let error_msg = || format!("failed to scroll down in plugin {}", env.plugin_env.name()); let action = Action::ScrollDown; apply_action!(action, error_msg, env); } -fn host_scroll_to_top(env: &ForeignFunctionEnv) { +fn scroll_to_top(env: &ForeignFunctionEnv) { let error_msg = || format!("failed to scroll in plugin {}", env.plugin_env.name()); let action = Action::ScrollToTop; apply_action!(action, error_msg, env); } -fn host_scroll_to_bottom(env: &ForeignFunctionEnv) { +fn scroll_to_bottom(env: &ForeignFunctionEnv) { let error_msg = || format!("failed to scroll in plugin {}", env.plugin_env.name()); let action = Action::ScrollToBottom; apply_action!(action, error_msg, env); } -fn host_page_scroll_up(env: &ForeignFunctionEnv) { +fn page_scroll_up(env: &ForeignFunctionEnv) { let error_msg = || format!("failed to scroll in plugin {}", env.plugin_env.name()); let action = Action::PageScrollUp; apply_action!(action, error_msg, env); } -fn host_page_scroll_down(env: &ForeignFunctionEnv) { +fn page_scroll_down(env: &ForeignFunctionEnv) { let error_msg = || format!("failed to scroll in plugin {}", env.plugin_env.name()); let action = Action::PageScrollDown; apply_action!(action, error_msg, env); } -fn host_toggle_focus_fullscreen(env: &ForeignFunctionEnv) { +fn toggle_focus_fullscreen(env: &ForeignFunctionEnv) { let error_msg = || { format!( "failed to toggle full screen in plugin {}", @@ -811,7 +724,7 @@ fn host_toggle_focus_fullscreen(env: &ForeignFunctionEnv) { apply_action!(action, error_msg, env); } -fn host_toggle_pane_frames(env: &ForeignFunctionEnv) { +fn toggle_pane_frames(env: &ForeignFunctionEnv) { let error_msg = || { format!( "failed to toggle full screen in plugin {}", @@ -822,7 +735,7 @@ fn host_toggle_pane_frames(env: &ForeignFunctionEnv) { apply_action!(action, error_msg, env); } -fn host_toggle_pane_embed_or_eject(env: &ForeignFunctionEnv) { +fn toggle_pane_embed_or_eject(env: &ForeignFunctionEnv) { let error_msg = || { format!( "failed to toggle pane embed or eject in plugin {}", @@ -833,7 +746,7 @@ fn host_toggle_pane_embed_or_eject(env: &ForeignFunctionEnv) { apply_action!(action, error_msg, env); } -fn host_undo_rename_pane(env: &ForeignFunctionEnv) { +fn undo_rename_pane(env: &ForeignFunctionEnv) { let error_msg = || { format!( "failed to undo rename pane in plugin {}", @@ -844,7 +757,7 @@ fn host_undo_rename_pane(env: &ForeignFunctionEnv) { apply_action!(action, error_msg, env); } -fn host_close_focus(env: &ForeignFunctionEnv) { +fn close_focus(env: &ForeignFunctionEnv) { let error_msg = || { format!( "failed to close focused pane in plugin {}", @@ -855,7 +768,7 @@ fn host_close_focus(env: &ForeignFunctionEnv) { apply_action!(action, error_msg, env); } -fn host_toggle_active_tab_sync(env: &ForeignFunctionEnv) { +fn toggle_active_tab_sync(env: &ForeignFunctionEnv) { let error_msg = || { format!( "failed to toggle active tab sync in plugin {}", @@ -866,7 +779,7 @@ fn host_toggle_active_tab_sync(env: &ForeignFunctionEnv) { apply_action!(action, error_msg, env); } -fn host_close_focused_tab(env: &ForeignFunctionEnv) { +fn close_focused_tab(env: &ForeignFunctionEnv) { let error_msg = || { format!( "failed to close active tab in plugin {}", @@ -877,7 +790,7 @@ fn host_close_focused_tab(env: &ForeignFunctionEnv) { apply_action!(action, error_msg, env); } -fn host_undo_rename_tab(env: &ForeignFunctionEnv) { +fn undo_rename_tab(env: &ForeignFunctionEnv) { let error_msg = || { format!( "failed to undo rename tab in plugin {}", @@ -888,13 +801,13 @@ fn host_undo_rename_tab(env: &ForeignFunctionEnv) { apply_action!(action, error_msg, env); } -fn host_quit_zellij(env: &ForeignFunctionEnv) { +fn quit_zellij(env: &ForeignFunctionEnv) { let error_msg = || format!("failed to quit zellij in plugin {}", env.plugin_env.name()); let action = Action::Quit; apply_action!(action, error_msg, env); } -fn host_previous_swap_layout(env: &ForeignFunctionEnv) { +fn previous_swap_layout(env: &ForeignFunctionEnv) { let error_msg = || { format!( "failed to switch swap layout in plugin {}", @@ -905,7 +818,7 @@ fn host_previous_swap_layout(env: &ForeignFunctionEnv) { apply_action!(action, error_msg, env); } -fn host_next_swap_layout(env: &ForeignFunctionEnv) { +fn next_swap_layout(env: &ForeignFunctionEnv) { let error_msg = || { format!( "failed to switch swap layout in plugin {}", @@ -916,49 +829,37 @@ fn host_next_swap_layout(env: &ForeignFunctionEnv) { apply_action!(action, error_msg, env); } -fn host_go_to_tab_name(env: &ForeignFunctionEnv) { +fn go_to_tab_name(env: &ForeignFunctionEnv, tab_name: String) { let error_msg = || format!("failed to change tab in plugin {}", env.plugin_env.name()); - wasi_read_string(&env.plugin_env.wasi_env) - .and_then(|tab_name| { - let create = false; - let action = Action::GoToTabName(tab_name, create); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let create = false; + let action = Action::GoToTabName(tab_name, create); + apply_action!(action, error_msg, env); } -fn host_focus_or_create_tab(env: &ForeignFunctionEnv) { +fn focus_or_create_tab(env: &ForeignFunctionEnv, tab_name: String) { let error_msg = || { format!( "failed to change or create tab in plugin {}", env.plugin_env.name() ) }; - wasi_read_string(&env.plugin_env.wasi_env) - .and_then(|tab_name| { - let create = true; - let action = Action::GoToTabName(tab_name, create); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let create = true; + let action = Action::GoToTabName(tab_name, create); + apply_action!(action, error_msg, env); } -fn host_go_to_tab(env: &ForeignFunctionEnv, tab_index: i32) { +fn go_to_tab(env: &ForeignFunctionEnv, tab_index: u32) { let error_msg = || { format!( "failed to change tab focus in plugin {}", env.plugin_env.name() ) }; - let action = Action::GoToTab(tab_index as u32); + let action = Action::GoToTab(tab_index); apply_action!(action, error_msg, env); } -fn host_start_or_reload_plugin(env: &ForeignFunctionEnv) { +fn start_or_reload_plugin(env: &ForeignFunctionEnv, url: &str) -> Result<()> { let error_msg = || { format!( "failed to start or reload plugin in plugin {}", @@ -966,106 +867,74 @@ fn host_start_or_reload_plugin(env: &ForeignFunctionEnv) { ) }; let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - wasi_read_string(&env.plugin_env.wasi_env) - .and_then(|url| Url::parse(&url).map_err(|e| anyhow!("Failed to parse url: {}", e))) - .and_then(|url| { - RunPluginLocation::parse(url.as_str(), Some(cwd)) - .map_err(|e| anyhow!("Failed to parse plugin location: {}", e)) - }) - .and_then(|run_plugin_location| { - let run_plugin = RunPlugin { - location: run_plugin_location, - _allow_exec_host_cmd: false, - configuration: PluginUserConfiguration::new(BTreeMap::new()), // TODO: allow passing configuration - }; - let action = Action::StartOrReloadPlugin(run_plugin); - apply_action!(action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let url = Url::parse(&url).map_err(|e| anyhow!("Failed to parse url: {}", e))?; + let run_plugin_location = RunPluginLocation::parse(url.as_str(), Some(cwd)) + .map_err(|e| anyhow!("Failed to parse plugin location: {}", e))?; + let run_plugin = RunPlugin { + location: run_plugin_location, + _allow_exec_host_cmd: false, + configuration: PluginUserConfiguration::new(BTreeMap::new()), // TODO: allow passing configuration + }; + let action = Action::StartOrReloadPlugin(run_plugin); + apply_action!(action, error_msg, env); + Ok(()) } -fn host_close_terminal_pane(env: &ForeignFunctionEnv, terminal_pane_id: i32) { +fn close_terminal_pane(env: &ForeignFunctionEnv, terminal_pane_id: u32) { let error_msg = || { format!( "failed to change tab focus in plugin {}", env.plugin_env.name() ) }; - let action = Action::CloseTerminalPane(terminal_pane_id as u32); + let action = Action::CloseTerminalPane(terminal_pane_id); apply_action!(action, error_msg, env); } -fn host_close_plugin_pane(env: &ForeignFunctionEnv, plugin_pane_id: i32) { +fn close_plugin_pane(env: &ForeignFunctionEnv, plugin_pane_id: u32) { let error_msg = || { format!( "failed to change tab focus in plugin {}", env.plugin_env.name() ) }; - let action = Action::ClosePluginPane(plugin_pane_id as u32); + let action = Action::ClosePluginPane(plugin_pane_id); apply_action!(action, error_msg, env); } -fn host_focus_terminal_pane( +fn focus_terminal_pane( env: &ForeignFunctionEnv, - terminal_pane_id: i32, - should_float_if_hidden: i32, + terminal_pane_id: u32, + should_float_if_hidden: bool, ) { - let should_float_if_hidden = should_float_if_hidden != 0; - let action = Action::FocusTerminalPaneWithId(terminal_pane_id as u32, should_float_if_hidden); + let action = Action::FocusTerminalPaneWithId(terminal_pane_id, should_float_if_hidden); let error_msg = || format!("Failed to focus terminal pane"); apply_action!(action, error_msg, env); } -fn host_focus_plugin_pane( - env: &ForeignFunctionEnv, - plugin_pane_id: i32, - should_float_if_hidden: i32, -) { - let should_float_if_hidden = should_float_if_hidden != 0; - let action = Action::FocusPluginPaneWithId(plugin_pane_id as u32, should_float_if_hidden); +fn focus_plugin_pane(env: &ForeignFunctionEnv, plugin_pane_id: u32, should_float_if_hidden: bool) { + let action = Action::FocusPluginPaneWithId(plugin_pane_id, should_float_if_hidden); let error_msg = || format!("Failed to focus plugin pane"); apply_action!(action, error_msg, env); } -fn host_rename_terminal_pane(env: &ForeignFunctionEnv) { +fn rename_terminal_pane(env: &ForeignFunctionEnv, terminal_pane_id: u32, new_name: &str) { let error_msg = || format!("Failed to rename terminal pane"); - wasi_read_object::<(u32, String)>(&env.plugin_env.wasi_env) - .and_then(|(terminal_pane_id, new_name)| { - let rename_pane_action = - Action::RenameTerminalPane(terminal_pane_id, new_name.as_bytes().to_vec()); - apply_action!(rename_pane_action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let rename_pane_action = + Action::RenameTerminalPane(terminal_pane_id, new_name.as_bytes().to_vec()); + apply_action!(rename_pane_action, error_msg, env); } -fn host_rename_plugin_pane(env: &ForeignFunctionEnv) { +fn rename_plugin_pane(env: &ForeignFunctionEnv, plugin_pane_id: u32, new_name: &str) { let error_msg = || format!("Failed to rename plugin pane"); - wasi_read_object::<(u32, String)>(&env.plugin_env.wasi_env) - .and_then(|(plugin_pane_id, new_name)| { - let rename_pane_action = - Action::RenamePluginPane(plugin_pane_id, new_name.as_bytes().to_vec()); - apply_action!(rename_pane_action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let rename_pane_action = Action::RenamePluginPane(plugin_pane_id, new_name.as_bytes().to_vec()); + apply_action!(rename_pane_action, error_msg, env); } -fn host_rename_tab(env: &ForeignFunctionEnv) { +fn rename_tab(env: &ForeignFunctionEnv, tab_index: u32, new_name: &str) { let error_msg = || format!("Failed to rename tab"); - wasi_read_object::<(u32, String)>(&env.plugin_env.wasi_env) - .and_then(|(tab_index, new_name)| { - let rename_tab_action = Action::RenameTab(tab_index, new_name.as_bytes().to_vec()); - apply_action!(rename_tab_action, error_msg, env); - Ok(()) - }) - .with_context(error_msg) - .fatal(); + let rename_tab_action = Action::RenameTab(tab_index, new_name.as_bytes().to_vec()); + apply_action!(rename_tab_action, error_msg, env); } // Custom panic handler for plugins. @@ -1073,19 +942,11 @@ fn host_rename_tab(env: &ForeignFunctionEnv) { // This is called when a panic occurs in a plugin. Since most panics will likely originate in the // code trying to deserialize an `Event` upon a plugin state update, we read some panic message, // formatted as string from the plugin. -fn host_report_panic(env: &ForeignFunctionEnv) { - let msg = wasi_read_string(&env.plugin_env.wasi_env) - .with_context(|| { - format!( - "failed to report panic for plugin '{}'", - env.plugin_env.name() - ) - }) - .fatal(); - log::error!("PANIC IN PLUGIN! {}", msg); +fn report_panic(env: &ForeignFunctionEnv, msg: &str) { + log::error!("PANIC IN PLUGIN!\n\r{}", msg); handle_plugin_crash( env.plugin_env.plugin_id, - msg, + msg.to_owned(), env.plugin_env.senders.clone(), ); } @@ -1135,7 +996,7 @@ pub fn wasi_write_object(wasi_env: &WasiEnv, object: &(impl Serialize + ?Sized)) .with_context(|| format!("failed to serialize object for WASI env '{wasi_env:?}'")) } -pub fn wasi_read_object(wasi_env: &WasiEnv) -> Result { +pub fn wasi_read_bytes(wasi_env: &WasiEnv) -> Result> { wasi_read_string(wasi_env) .and_then(|string| serde_json::from_str(&string).map_err(anyError::new)) .with_context(|| format!("failed to deserialize object from WASI env '{wasi_env:?}'")) diff --git a/zellij-tile/src/lib.rs b/zellij-tile/src/lib.rs index 7bcaf47383..67b4a551cd 100644 --- a/zellij-tile/src/lib.rs +++ b/zellij-tile/src/lib.rs @@ -22,6 +22,8 @@ use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use zellij_utils::data::Event; +// use zellij_tile::shim::plugin_api::event::ProtobufEvent; + /// This trait should be implemented - once per plugin - on a struct (normally representing the /// plugin state). This struct should then be registered with the /// [`register_plugin!`](register_plugin) macro. @@ -104,22 +106,31 @@ macro_rules! register_plugin { #[no_mangle] fn load() { STATE.with(|state| { - let configuration = $crate::shim::object_from_stdin() - .context($crate::PLUGIN_MISMATCH) - .to_stdout() - .unwrap(); - state.borrow_mut().load(configuration); + use std::collections::BTreeMap; + use std::convert::TryInto; + use zellij_tile::shim::plugin_api::action::ProtobufPluginConfiguration; + use zellij_tile::shim::prost::Message; + let protobuf_bytes: Vec = $crate::shim::object_from_stdin().unwrap(); + let protobuf_configuration: ProtobufPluginConfiguration = + ProtobufPluginConfiguration::decode(protobuf_bytes.as_slice()).unwrap(); + let plugin_configuration: BTreeMap = + BTreeMap::try_from(&protobuf_configuration).unwrap(); + state.borrow_mut().load(plugin_configuration); }); } #[no_mangle] pub fn update() -> bool { + let err_context = "Failed to deserialize event"; + use std::convert::TryInto; + use zellij_tile::shim::plugin_api::event::ProtobufEvent; + use zellij_tile::shim::prost::Message; STATE.with(|state| { - let object = $crate::shim::object_from_stdin() - .context($crate::PLUGIN_MISMATCH) - .to_stdout() - .unwrap(); - state.borrow_mut().update(object) + let protobuf_bytes: Vec = $crate::shim::object_from_stdin().unwrap(); + let protobuf_event: ProtobufEvent = + ProtobufEvent::decode(protobuf_bytes.as_slice()).unwrap(); + let event = protobuf_event.try_into().unwrap(); + state.borrow_mut().update(event) }) } @@ -168,18 +179,15 @@ macro_rules! register_worker { } #[no_mangle] pub fn $worker_name() { - + use zellij_tile::shim::plugin_api::message::ProtobufMessage; + use zellij_tile::shim::prost::Message; let worker_display_name = std::stringify!($worker_name); - - // read message from STDIN - let (message, payload): (String, String) = $crate::shim::object_from_stdin() - .unwrap_or_else(|e| { - eprintln!( - "Failed to deserialize message to worker \"{}\": {:?}", - worker_display_name, e - ); - Default::default() - }); + let protobuf_bytes: Vec = $crate::shim::object_from_stdin() + .unwrap(); + let protobuf_message: ProtobufMessage = ProtobufMessage::decode(protobuf_bytes.as_slice()) + .unwrap(); + let message = protobuf_message.name; + let payload = protobuf_message.payload; $worker_static_name.with(|worker_instance| { let mut worker_instance = worker_instance.borrow_mut(); worker_instance.on_message(message, payload); diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index c12a615289..c3a772f96d 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -1,375 +1,559 @@ use serde::{de::DeserializeOwned, Serialize}; +use std::collections::HashSet; use std::{io, path::Path}; use zellij_utils::data::*; use zellij_utils::errors::prelude::*; +pub use zellij_utils::plugin_api; +use zellij_utils::plugin_api::plugin_command::ProtobufPluginCommand; +use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion}; + +pub use zellij_utils::prost::{self, *}; // Subscription Handling /// Subscribe to a list of [`Event`]s represented by their [`EventType`]s that will then trigger the `update` method pub fn subscribe(event_types: &[EventType]) { - object_to_stdout(&event_types); - unsafe { host_subscribe() }; + let event_types: HashSet = event_types.iter().cloned().collect(); + let plugin_command = PluginCommand::Subscribe(event_types); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Unsubscribe to a list of [`Event`]s represented by their [`EventType`]s. pub fn unsubscribe(event_types: &[EventType]) { - object_to_stdout(&event_types); - unsafe { host_unsubscribe() }; + let event_types: HashSet = event_types.iter().cloned().collect(); + let plugin_command = PluginCommand::Unsubscribe(event_types); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } // Plugin Settings /// Sets the plugin as selectable or unselectable to the user. Unselectable plugins might be desired when they do not accept user input. pub fn set_selectable(selectable: bool) { - unsafe { host_set_selectable(selectable as i32) }; + let plugin_command = PluginCommand::SetSelectable(selectable); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } // Query Functions /// Returns the unique Zellij pane ID for the plugin as well as the Zellij process id. pub fn get_plugin_ids() -> PluginIds { - unsafe { host_get_plugin_ids() }; - object_from_stdin().unwrap() + let plugin_command = PluginCommand::GetPluginIds; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; + let protobuf_plugin_ids = + ProtobufPluginIds::decode(bytes_from_stdin().unwrap().as_slice()).unwrap(); + PluginIds::try_from(protobuf_plugin_ids).unwrap() } /// Returns the version of the running Zellij instance - can be useful to check plugin compatibility pub fn get_zellij_version() -> String { - unsafe { host_get_zellij_version() }; - object_from_stdin().unwrap() + let plugin_command = PluginCommand::GetZellijVersion; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; + let protobuf_zellij_version = + ProtobufZellijVersion::decode(bytes_from_stdin().unwrap().as_slice()).unwrap(); + protobuf_zellij_version.version } // Host Functions /// Open a file in the user's default `$EDITOR` in a new pane -pub fn open_file>(path: P) { - object_to_stdout(&path.as_ref()); - unsafe { host_open_file() }; +pub fn open_file(file_to_open: FileToOpen) { + let plugin_command = PluginCommand::OpenFile(file_to_open); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Open a file in the user's default `$EDITOR` in a new floating pane -pub fn open_file_floating>(path: P) { - object_to_stdout(&path.as_ref()); - unsafe { host_open_file_floating() }; -} - -/// Open a file to a specific line in the user's default `$EDITOR` (if it supports it, most do) in a new pane -pub fn open_file_with_line>(path: P, line: usize) { - object_to_stdout(&(path.as_ref(), line)); - unsafe { host_open_file_with_line() }; -} - -/// Open a file to a specific line in the user's default `$EDITOR` (if it supports it, most do) in a new floating pane -pub fn open_file_with_line_floating>(path: P, line: usize) { - object_to_stdout(&(path.as_ref(), line)); - unsafe { host_open_file_with_line_floating() }; +pub fn open_file_floating(file_to_open: FileToOpen) { + let plugin_command = PluginCommand::OpenFileFloating(file_to_open); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Open a new terminal pane to the specified location on the host filesystem pub fn open_terminal>(path: P) { - object_to_stdout(&path.as_ref()); - unsafe { host_open_terminal() }; + let file_to_open = FileToOpen::new(path.as_ref().to_path_buf()); + let plugin_command = PluginCommand::OpenTerminal(file_to_open); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Open a new floating terminal pane to the specified location on the host filesystem pub fn open_terminal_floating>(path: P) { - object_to_stdout(&path.as_ref()); - unsafe { host_open_terminal_floating() }; + let file_to_open = FileToOpen::new(path.as_ref().to_path_buf()); + let plugin_command = PluginCommand::OpenTerminalFloating(file_to_open); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Open a new command pane with the specified command and args (this sort of pane allows the user to control the command, re-run it and see its exit status through the Zellij UI). -pub fn open_command_pane, A: AsRef>(path: P, args: Vec) { - object_to_stdout(&( - path.as_ref(), - args.iter().map(|a| a.as_ref()).collect::>(), - )); - unsafe { host_open_command_pane() }; +// pub fn open_command_pane, A: AsRef>(path: P, args: Vec) { +pub fn open_command_pane(command_to_run: CommandToRun) { + let plugin_command = PluginCommand::OpenCommandPane(command_to_run); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Open a new floating command pane with the specified command and args (this sort of pane allows the user to control the command, re-run it and see its exit status through the Zellij UI). -pub fn open_command_pane_floating, A: AsRef>(path: P, args: Vec) { - object_to_stdout(&( - path.as_ref(), - args.iter().map(|a| a.as_ref()).collect::>(), - )); - unsafe { host_open_command_pane_floating() }; +// pub fn open_command_pane_floating, A: AsRef>(path: P, args: Vec) { +pub fn open_command_pane_floating(command_to_run: CommandToRun) { + let plugin_command = PluginCommand::OpenCommandPaneFloating(command_to_run); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change the focused tab to the specified index (corresponding with the default tab names, to starting at `1`, `0` will be considered as `1`). pub fn switch_tab_to(tab_idx: u32) { - unsafe { host_switch_tab_to(tab_idx) }; + let plugin_command = PluginCommand::SwitchTabTo(tab_idx); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Set a timeout in seconds (or fractions thereof) after which the plugins [update](./plugin-api-events#update) method will be called with the [`Timer`](./plugin-api-events.md#timer) event. pub fn set_timeout(secs: f64) { - unsafe { host_set_timeout(secs) }; + let plugin_command = PluginCommand::SetTimeout(secs as f32); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } #[doc(hidden)] pub fn exec_cmd(cmd: &[&str]) { - object_to_stdout(&cmd); - unsafe { host_exec_cmd() }; + let plugin_command = + PluginCommand::ExecCmd(cmd.iter().cloned().map(|s| s.to_owned()).collect()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Hide the plugin pane (suppress it) from the UI pub fn hide_self() { - unsafe { host_hide_self() }; + let plugin_command = PluginCommand::HideSelf; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Show the plugin pane (unsuppress it if it is suppressed), focus it and switch to its tab pub fn show_self(should_float_if_hidden: bool) { - unsafe { host_show_self(should_float_if_hidden as i32) }; + let plugin_command = PluginCommand::ShowSelf(should_float_if_hidden); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Switch to the specified Input Mode (eg. `Normal`, `Tab`, `Pane`) pub fn switch_to_input_mode(mode: &InputMode) { - object_to_stdout(&mode); - unsafe { host_switch_to_mode() }; + let plugin_command = PluginCommand::SwitchToMode(*mode); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Provide a stringified [`layout`](https://zellij.dev/documentation/layouts.html) to be applied to the current session. If the layout has multiple tabs, they will all be opened. pub fn new_tabs_with_layout(layout: &str) { - println!("{}", layout); - unsafe { host_new_tabs_with_layout() } + let plugin_command = PluginCommand::NewTabsWithLayout(layout.to_owned()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Open a new tab with the default layout pub fn new_tab() { - unsafe { host_new_tab() } + let plugin_command = PluginCommand::NewTab; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change focus to the next tab or loop back to the first pub fn go_to_next_tab() { - unsafe { host_go_to_next_tab() } + let plugin_command = PluginCommand::GoToNextTab; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change focus to the previous tab or loop back to the last pub fn go_to_previous_tab() { - unsafe { host_go_to_previous_tab() } + let plugin_command = PluginCommand::GoToPreviousTab; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } pub fn report_panic(info: &std::panic::PanicInfo) { - println!(""); - println!("A panic occured in a plugin"); - println!("{:#?}", info); - unsafe { host_report_panic() }; + let panic_payload = if let Some(s) = info.payload().downcast_ref::<&str>() { + format!("{}", s) + } else { + format!("") + }; + let panic_stringified = format!("{}\n\r{:#?}", panic_payload, info).replace("\n", "\r\n"); + let plugin_command = PluginCommand::ReportPanic(panic_stringified); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Either Increase or Decrease the size of the focused pane pub fn resize_focused_pane(resize: Resize) { - object_to_stdout(&resize); - unsafe { host_resize() }; + let plugin_command = PluginCommand::Resize(resize); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Either Increase or Decrease the size of the focused pane in a specified direction (eg. `Left`, `Right`, `Up`, `Down`). pub fn resize_focused_pane_with_direction(resize: Resize, direction: Direction) { - object_to_stdout(&(resize, direction)); - unsafe { host_resize_with_direction() }; + let resize_strategy = ResizeStrategy { + resize, + direction: Some(direction), + invert_on_boundaries: false, + }; + let plugin_command = PluginCommand::ResizeWithDirection(resize_strategy); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change focus tot he next pane in chronological order pub fn focus_next_pane() { - unsafe { host_focus_next_pane() }; + let plugin_command = PluginCommand::FocusNextPane; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change focus to the previous pane in chronological order pub fn focus_previous_pane() { - unsafe { host_focus_previous_pane() }; + let plugin_command = PluginCommand::FocusPreviousPane; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change the focused pane in the specified direction pub fn move_focus(direction: Direction) { - object_to_stdout(&direction); - unsafe { host_move_focus() }; + let plugin_command = PluginCommand::MoveFocus(direction); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change the focused pane in the specified direction, if the pane is on the edge of the screen, the next tab is focused (next if right edge, previous if left edge). pub fn move_focus_or_tab(direction: Direction) { - object_to_stdout(&direction); - unsafe { host_move_focus_or_tab() }; + let plugin_command = PluginCommand::MoveFocusOrTab(direction); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Detach the user from the active session pub fn detach() { - unsafe { host_detach() }; + let plugin_command = PluginCommand::Detach; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Edit the scrollback of the focused pane in the user's default `$EDITOR` pub fn edit_scrollback() { - unsafe { host_edit_scrollback() }; + let plugin_command = PluginCommand::EditScrollback; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Write bytes to the `STDIN` of the focused pane pub fn write(bytes: Vec) { - object_to_stdout(&bytes); - unsafe { host_write() }; + let plugin_command = PluginCommand::Write(bytes); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Write characters to the `STDIN` of the focused pane pub fn write_chars(chars: &str) { - println!("{}", chars); - unsafe { host_write_chars() }; + let plugin_command = PluginCommand::WriteChars(chars.to_owned()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Focused the previously focused tab (regardless of the tab position) pub fn toggle_tab() { - unsafe { host_toggle_tab() }; + let plugin_command = PluginCommand::ToggleTab; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Switch the position of the focused pane with a different pane pub fn move_pane() { - unsafe { host_move_pane() }; + let plugin_command = PluginCommand::MovePane; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Switch the position of the focused pane with a different pane in the specified direction (eg. `Down`, `Up`, `Left`, `Right`). pub fn move_pane_with_direction(direction: Direction) { - object_to_stdout(&direction); - unsafe { host_move_pane_with_direction() }; + let plugin_command = PluginCommand::MovePaneWithDirection(direction); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Clear the scroll buffer of the focused pane pub fn clear_screen() { - unsafe { host_clear_screen() }; + let plugin_command = PluginCommand::ClearScreen; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Scroll the focused pane up 1 line pub fn scroll_up() { - unsafe { host_scroll_up() }; + let plugin_command = PluginCommand::ScrollUp; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Scroll the focused pane down 1 line pub fn scroll_down() { - unsafe { host_scroll_down() }; + let plugin_command = PluginCommand::ScrollDown; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Scroll the focused pane all the way to the top of the scrollbuffer pub fn scroll_to_top() { - unsafe { host_scroll_to_top() }; + let plugin_command = PluginCommand::ScrollToTop; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Scroll the focused pane all the way to the bottom of the scrollbuffer pub fn scroll_to_bottom() { - unsafe { host_scroll_to_bottom() }; + let plugin_command = PluginCommand::ScrollToBottom; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Scroll the focused pane up one page pub fn page_scroll_up() { - unsafe { host_page_scroll_up() }; + let plugin_command = PluginCommand::PageScrollUp; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Scroll the focused pane down one page pub fn page_scroll_down() { - unsafe { host_page_scroll_down() }; + let plugin_command = PluginCommand::PageScrollDown; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Toggle the focused pane to be fullscreen or normal sized pub fn toggle_focus_fullscreen() { - unsafe { host_toggle_focus_fullscreen() }; + let plugin_command = PluginCommand::ToggleFocusFullscreen; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Toggle the UI pane frames on or off pub fn toggle_pane_frames() { - unsafe { host_toggle_pane_frames() }; + let plugin_command = PluginCommand::TogglePaneFrames; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Embed the currently focused pane (make it stop floating) or turn it to a float pane if it is not pub fn toggle_pane_embed_or_eject() { - unsafe { host_toggle_pane_embed_or_eject() }; + let plugin_command = PluginCommand::TogglePaneEmbedOrEject; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } pub fn undo_rename_pane() { - unsafe { host_undo_rename_pane() }; + let plugin_command = PluginCommand::UndoRenamePane; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Close the focused pane pub fn close_focus() { - unsafe { host_close_focus() }; + let plugin_command = PluginCommand::CloseFocus; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Turn the `STDIN` synchronization of the current tab on or off pub fn toggle_active_tab_sync() { - unsafe { host_toggle_active_tab_sync() }; + let plugin_command = PluginCommand::ToggleActiveTabSync; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Close the focused tab pub fn close_focused_tab() { - unsafe { host_close_focused_tab() }; + let plugin_command = PluginCommand::CloseFocusedTab; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } pub fn undo_rename_tab() { - unsafe { host_undo_rename_tab() }; + let plugin_command = PluginCommand::UndoRenameTab; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Compeltely quit Zellij for this and all other connected clients pub fn quit_zellij() { - unsafe { host_quit_zellij() }; + let plugin_command = PluginCommand::QuitZellij; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change to the previous [swap layout](https://zellij.dev/documentation/swap-layouts.html) pub fn previous_swap_layout() { - unsafe { host_previous_swap_layout() }; + let plugin_command = PluginCommand::PreviousSwapLayout; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change to the next [swap layout](https://zellij.dev/documentation/swap-layouts.html) pub fn next_swap_layout() { - unsafe { host_next_swap_layout() }; + let plugin_command = PluginCommand::NextSwapLayout; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change focus to the tab with the specified name pub fn go_to_tab_name(tab_name: &str) { - println!("{}", tab_name); - unsafe { host_go_to_tab_name() }; + let plugin_command = PluginCommand::GoToTabName(tab_name.to_owned()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Change focus to the tab with the specified name or create it if it does not exist pub fn focus_or_create_tab(tab_name: &str) { - print!("{}", tab_name); - unsafe { host_focus_or_create_tab() }; + let plugin_command = PluginCommand::FocusOrCreateTab(tab_name.to_owned()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } -pub fn go_to_tab(tab_index: i32) { - unsafe { host_go_to_tab(tab_index) }; +pub fn go_to_tab(tab_index: u32) { + let plugin_command = PluginCommand::GoToTab(tab_index); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } pub fn start_or_reload_plugin(url: &str) { - println!("{}", url); - unsafe { host_start_or_reload_plugin() }; + let plugin_command = PluginCommand::StartOrReloadPlugin(url.to_owned()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Closes a terminal pane with the specified id -pub fn close_terminal_pane(terminal_pane_id: i32) { - unsafe { host_close_terminal_pane(terminal_pane_id) }; +pub fn close_terminal_pane(terminal_pane_id: u32) { + let plugin_command = PluginCommand::CloseTerminalPane(terminal_pane_id); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Closes a plugin pane with the specified id -pub fn close_plugin_pane(plugin_pane_id: i32) { - unsafe { host_close_plugin_pane(plugin_pane_id) }; +pub fn close_plugin_pane(plugin_pane_id: u32) { + let plugin_command = PluginCommand::ClosePluginPane(plugin_pane_id); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Changes the focus to the terminal pane with the specified id, unsuppressing it if it was suppressed and switching to its tab and layer (eg. floating/tiled). -pub fn focus_terminal_pane(terminal_pane_id: i32, should_float_if_hidden: bool) { - unsafe { host_focus_terminal_pane(terminal_pane_id, should_float_if_hidden as i32) }; +pub fn focus_terminal_pane(terminal_pane_id: u32, should_float_if_hidden: bool) { + let plugin_command = PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Changes the focus to the plugin pane with the specified id, unsuppressing it if it was suppressed and switching to its tab and layer (eg. floating/tiled). -pub fn focus_plugin_pane(plugin_pane_id: i32, should_float_if_hidden: bool) { - unsafe { host_focus_plugin_pane(plugin_pane_id, should_float_if_hidden as i32) }; +pub fn focus_plugin_pane(plugin_pane_id: u32, should_float_if_hidden: bool) { + let plugin_command = PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Changes the name (the title that appears in the UI) of the terminal pane with the specified id. -pub fn rename_terminal_pane>(terminal_pane_id: i32, new_name: S) { - object_to_stdout(&(terminal_pane_id, new_name.as_ref())); - unsafe { host_rename_terminal_pane() }; +pub fn rename_terminal_pane>(terminal_pane_id: u32, new_name: S) +where + S: ToString, +{ + let plugin_command = PluginCommand::RenameTerminalPane(terminal_pane_id, new_name.to_string()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Changes the name (the title that appears in the UI) of the plugin pane with the specified id. -pub fn rename_plugin_pane>(plugin_pane_id: i32, new_name: S) { - object_to_stdout(&(plugin_pane_id, new_name.as_ref())); - unsafe { host_rename_plugin_pane() }; +pub fn rename_plugin_pane>(plugin_pane_id: u32, new_name: S) +where + S: ToString, +{ + let plugin_command = PluginCommand::RenamePluginPane(plugin_pane_id, new_name.to_string()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Changes the name (the title that appears in the UI) of the tab with the specified position. -pub fn rename_tab>(tab_position: i32, new_name: S) { - object_to_stdout(&(tab_position, new_name.as_ref())); - unsafe { host_rename_tab() }; +pub fn rename_tab>(tab_position: u32, new_name: S) +where + S: ToString, +{ + let plugin_command = PluginCommand::RenameTab(tab_position, new_name.to_string()); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } // Utility Functions @@ -408,6 +592,14 @@ pub fn object_from_stdin() -> Result { serde_json::from_str(&json).with_context(err_context) } +#[doc(hidden)] +pub fn bytes_from_stdin() -> Result> { + let err_context = || "failed to deserialize bytes from stdin".to_string(); + let mut json = String::new(); + io::stdin().read_line(&mut json).with_context(err_context)?; + serde_json::from_str(&json).with_context(err_context) +} + #[doc(hidden)] pub fn object_to_stdout(object: &impl Serialize) { // TODO: no crashy @@ -415,91 +607,22 @@ pub fn object_to_stdout(object: &impl Serialize) { } /// Post a message to a worker of this plugin, for more information please see [Plugin Workers](https://zellij.dev/documentation/plugin-api-workers.md) -pub fn post_message_to>(worker_name: S, message: S, payload: S) { - match serde_json::to_string(&(worker_name.as_ref(), message.as_ref(), payload.as_ref())) { - Ok(serialized) => println!("{}", serialized), - Err(e) => eprintln!("Failed to serialize message: {:?}", e), - } - unsafe { host_post_message_to() }; +pub fn post_message_to(plugin_message: PluginMessage) { + let plugin_command = PluginCommand::PostMessageTo(plugin_message); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } /// Post a message to this plugin, for more information please see [Plugin Workers](https://zellij.dev/documentation/plugin-api-workers.md) -pub fn post_message_to_plugin>(message: S, payload: S) { - match serde_json::to_string(&(message.as_ref(), payload.as_ref())) { - Ok(serialized) => println!("{}", serialized), - Err(e) => eprintln!("Failed to serialize message: {:?}", e), - } - unsafe { host_post_message_to_plugin() }; +pub fn post_message_to_plugin(plugin_message: PluginMessage) { + let plugin_command = PluginCommand::PostMessageToPlugin(plugin_message); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; } #[link(wasm_import_module = "zellij")] extern "C" { - fn host_subscribe(); - fn host_unsubscribe(); - fn host_set_selectable(selectable: i32); - fn host_get_plugin_ids(); - fn host_get_zellij_version(); - fn host_open_file(); - fn host_open_file_floating(); - fn host_open_file_with_line(); - fn host_open_file_with_line_floating(); - fn host_open_terminal(); - fn host_open_terminal_floating(); - fn host_open_command_pane(); - fn host_open_command_pane_floating(); - fn host_switch_tab_to(tab_idx: u32); - fn host_set_timeout(secs: f64); - fn host_exec_cmd(); - fn host_report_panic(); - fn host_post_message_to(); - fn host_post_message_to_plugin(); - fn host_hide_self(); - fn host_show_self(should_float_if_hidden: i32); - fn host_switch_to_mode(); - fn host_new_tabs_with_layout(); - fn host_new_tab(); - fn host_go_to_next_tab(); - fn host_go_to_previous_tab(); - fn host_resize(); - fn host_resize_with_direction(); - fn host_focus_next_pane(); - fn host_focus_previous_pane(); - fn host_move_focus(); - fn host_move_focus_or_tab(); - fn host_detach(); - fn host_edit_scrollback(); - fn host_write(); - fn host_write_chars(); - fn host_toggle_tab(); - fn host_move_pane(); - fn host_move_pane_with_direction(); - fn host_clear_screen(); - fn host_scroll_up(); - fn host_scroll_down(); - fn host_scroll_to_top(); - fn host_scroll_to_bottom(); - fn host_page_scroll_up(); - fn host_page_scroll_down(); - fn host_toggle_focus_fullscreen(); - fn host_toggle_pane_frames(); - fn host_toggle_pane_embed_or_eject(); - fn host_undo_rename_pane(); - fn host_close_focus(); - fn host_toggle_active_tab_sync(); - fn host_close_focused_tab(); - fn host_undo_rename_tab(); - fn host_quit_zellij(); - fn host_previous_swap_layout(); - fn host_next_swap_layout(); - fn host_go_to_tab_name(); - fn host_focus_or_create_tab(); - fn host_go_to_tab(tab_index: i32); - fn host_start_or_reload_plugin(); - fn host_close_terminal_pane(terminal_pane: i32); - fn host_close_plugin_pane(plugin_pane: i32); - fn host_focus_terminal_pane(terminal_pane: i32, should_float_if_hidden: i32); - fn host_focus_plugin_pane(plugin_pane: i32, should_float_if_hidden: i32); - fn host_rename_terminal_pane(); - fn host_rename_plugin_pane(); - fn host_rename_tab(); + fn host_run_plugin_command(); } diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index cd5988a91c..19e80e3043 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -42,6 +42,7 @@ shellexpand = "3.0.0" uuid = { version = "0.8.2", features = ["serde", "v4"] } async-channel = "1.8.0" include_dir = "0.7.3" +prost = "0.11.9" #[cfg(not(target_family = "wasm"))] [target.'cfg(not(target_family = "wasm"))'.dependencies] @@ -56,6 +57,9 @@ notify-debouncer-full = "0.1.0" insta = { version = "1.6.0", features = ["backtrace"] } expect-test = "1.4.1" +[build-dependencies] +prost-build = "0.11.9" + [features] # If this feature is NOT set (default): # - builtin plugins (status-bar, tab-bar, ...) are loaded directly from the application binary diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm index bd878f2a83fa7d7f69f1a77ab9046731c8ceef5a..f38c500fcb01a2172b61102aacd437694a3d8b6e 100755 GIT binary patch delta 106928 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 v�s%dq#%RB_+i^Br(M{a4enx%h~GUwovuO4}K~5z8hR&Pz@jP)YE?f?+pj5 delta 105893 zcmcG%33yb+(my=ieP)u$%!J7v5(sC8KoUsU3Hx%8u;UfC3%CIy=zs!luZkLwMZ|!> zAdP|&6crT(3^J&wAgF*S;L1f*P%faNqM~vY@%vTxIg<(Ez4!lq&&%_W?yjn?uCDH` zu3k>h?5O$eLUZ1HAG?!Pj&N7fzqL!>W@3uxbaXsF)rq@=FPU@0;G8r3=YsyZTrR;y zDn$uIaBgs~mwR2w0J&X4M3;%8Q3NL}sNFv9HhjU%dT!wZ+Fj4>=02C3C=&k(7ti7X zOi%{r2-RPd2NneP36$VU_IQk>Bu|nnD_UYSOv=j1t?%Pm(ZR+I(N~St;$!}CbdtMm zqqXnY2xUSH(upB$vxL!b=~Nzu6Ld0sT0TFH1?Y7uDJQC>uT+8J{XX7C(p|E5*}Bjj>NmaldA~VVpLi?#JAZyWey_>3+?<#l6n` zsQU@`Zud{(Y4`K)4?atL#Gm5#i^>IJp}0?M=U?&H`J4PL z{tSPb|C8_FhxoJn4Zf8>%~r8z**(VV>`k_lz0KZXyV()Ghy9a%&JMF(>??MReZzic zzp$U!4{X2tQ{yn7&lm70|47W_U$SS#6Jo7+RcsbV*)yVAY!I)A*Tfnj#Y*Pf@u+xQJSJ9(r^J(Dm3UgL7B%8Iu}(ZM){B^ULA)q7 zikHPg{)l+p*lhgMxQ}l&wix&DyZKRLJwI$5F{<1<#eVUL_*DENJ`)GTL2*+2Do%^v z#Fyd^@w+%B?lmg^C7w0b7;BB^jMc_V#tX)M?l+BX#t!2xW2f=9@uP9V_{q4(J==Y+ z`(tCj@rm&-<5%N13UG5F;kKHf0Uv&S&{gV4rca8fw_geQF_j~S_-5cE>xHr42-4D1Iy5Dy% zb1!wj>VDn*hI_U9A@>sZBKKDJ9{0oUkKAv$ce=lDf9Ag5{h(XA7rSHbgYE%zXKmX$#T9T_=-lNx5u80`$`n^{$~629xq?5EVx9%Q_Fan1ATHyZ>m z%NgY5#~+JMHe0go(WlLJeA8pmgJwArxuG%aa7|?>lks^E)Z8Cl#Tx!}7ZsgU%w3^W zpP7s>95hoSrr!(%PuE;t*pKm_?uxE#VzSfG?M=pYtX{kYf-fcUA*o^3vo-7a;KrKHMKgHD%hOS8DB#cX znZgPZ;+5#TCB4o9-xw`wb{Bg&`c$(6nRP3XVtPryhnwd$cGy7`!ZbR&f))!Ie&(~L zK8hx^np^XhL_cm`NGf5aji8cHX*>36bbeXm=&hysc5FjD_DeigUgltLvSZzC>L1G* z=vYnL7EL){vp9N5t8du$n*OaPa&|cSV%tLBVd@P&-|HTh9R0FwyCiqWFj>eI{iZ`s z^xN`ezva>ZggpAUcKr-Q+|i*Oi}=dq=+ur)qet6?>UpeNB4JMed`wqGz!z=Uz6|UK zwr`5R*SGJ=Jkizd8y4uIh`Z?tag%{QUiYdWqhGZzF11Ox)gVH>#>0D>+%hR124i$% z%Re6SctS4GQ?5NEGEl(zYPgY*v@b`gRruPC-WxDjt2N%hf1;NRI+v5+qHg+Mt*oT~`5m ztSjj*yBjrIw{E1(5#9RQ0zxjkB}g=G71}wsD!L=x3PJ7fUX)u~TCgG@P(-FpxTbp- zkWj%qq2M?)n$jZ$cqd(q_-#E_=P7pnOf-Q~N(QnTHbb*dj;?1t2b-3FJ+DszSWM_s zAAeW%Y{(AFJ2!}e=+T~j9c>1y#@JTpB&sYJUdzlmza}`{p43 z3w=X+{HXj7$@wn`faSp#1eKc8k4m)@f1^b7+J1y@{UVV?AEHEbO|Rea_t|qu+k4dC z6X&!?yjgFO@S@&-Lk90lqB9Z)2qY)GB#5>}1+~@Cif8}NRfJ-)`gTRhr#Fc5|3)Xs z@NwS|GW(S z44~2W_5i5A=E#6zh9Eeo1VOa(;B)c!p1~MVsWbNj?fLu{}C2y4dP^oQWnqjGXc6i~P%B)ZgWx%H?UGOntmp|q!REdI`y zInnc`+?sC>Dm9Y|OdrmeHiCy`t6vyYQPXJZGR{}r72UkGpytzQGYznfj+qsvU^aqd z3!=}eRL#4yE*1@zEukqV5S%^}*1sa)#l*VN0Y!UPRq&45W#%)*E;vp~0kT2lEtZAlbs2ozm za-s$I4@k&}z8BqaKUuNQ?$5^GpYLyszj=!~>9OL|E=jE-&19IsYZnax(W{Hv;xF_b zHZyG-tR_gi>=c6e=nsmImdXwwcEqJpYvk%?f7MvYA&*&43r+F|f`@CW7N7P}3stQM zA*fls!bk>_b*on_v#@L2$+8B4>rki_r+vaTZK%g3UAbwW9V}H{!||%ihloMW(10)QUi(w zl^1Bt6U?|B)8u5F{GpVg0bu*>Nt%+>Z@K9{hHJc2- zk;o>+pX@VP^tO!wc{-idhqk$vHTvYni{wq{Guit!Kc`m(NiHilTKIBFQD!I^!%)!R zAqO#xe&N#cW`?q~JRBZPbkfV&Wt5NbmdZ{ZLs;a)b0oz8hB%s?YM^qgeL15t7*4{j z2X;cVM4@C`bd+jK8Siwe_h*KvAsiOL*Bw1jmqMY`r9=Y$1HuzHj!mwE&96mU1{Mgy za`(xn?hQuKi(YA&206?mY9{zqSX99dT>ffl7kfwu%O62;hIL~kY$!k6O;m7;9qT2~ zd|BFZ!mlK#wwxKSlH~*|{Jr;8vSH7>+EjaS=sw64L=~YvDi~#c{VJK#dYd|#cF6`c zLV_s;5i$3BxxpYT2q`OAMS2J&Mfbdr1x6okB6FLvxl!Xff~xMA zMK!Qx#lQ{)WoRKx{gs=^ZAuh)$L1d$qkD!lJ~Ifd`D%?W)K+ukwVj?`bxfoZ$wPgU zTY=6l>N01;C)L5%{IvZ$9zOZ-R+!op+Zwq2!e2!C+8oa$zcqD|iJ!8d*z z{bYZ6&C<7T6wx*BUlYxLw`sJ;2a8jWk-{)LqF;;Pq3Az9XoJPyuy^yLZ9mMed2jOb ztmcaMZ()B)R@3&wbVF=|d(m&-V-(gL-*-Oq#;5M+!2QjF-em+g(=pFZ#E(A zLE1D}e)P@#nf%l@5N{@5bu@a_Cj(ej&6-aHYr5tk%4a(O0S;5(p*nI^#~sQ$ASP7D zdQ1U#J1zrp&WnaV%St-%3l%2hK{sn1z3#Igyc_PbwPs86no6S-ooDTVEWY~}ImBS) z(bEUM|NF6TDSzAxaYMZ)RDB$|7_1dcA?f``MeBAv77Qip6GdAKqV>1iTKa8GpU>}O z-bYp0q!}ge$uF|_CcEs4L)drwsix?kOIVM`S0*+zqtg5yM-kE0gmmVFB$7t=f_oGK z-6P75H1W>6*H)&gcJzWHo%xENYVJF75%a#MWCWp>BI(O4zUo`8fr>+g(GjOS(KX*R zmxBenAbRS{6@2Mwg{;~8)f+5t**(NJUP1IDmr29z#7{LFjvZo^-#$)wr<10j_1yl! z(1LYRA<#+HR2+cNZvP-cY+6e>a%@Cz%K%~^A*Me;2sF9MQDC`&7(j?43SoPcfqcsY zM1Mk@c%lgqfjK>Wd?if#^$!sN$6htzGzCv&tta1yiPrEV29`>rG?} zNXR(CV+DXXhY&B;$-%8uAbJsE9+eo+A*^~pScG`E4#IB*f#^xrPgs?+>J)>TC>$`V zOT@Vat#lxI5Vuon5{uzh1`yo|v8GNj+{y%^8zEk)gGjfsfapqy?JFx2d3mjDV7d_I zgF0CPRt^xI3GoFXte~wgOrAh=BE*jhk!~XjtolH7B*cs-DMyx#D7Er{=s<`EfvBwI zm1kkv^p_LnDU~I|&f>Nj0MVWhTUFaR12bSX1fm@ww$xFI&nf_-Eg{ws!m4Mp@>-36 zXhVouDo3T$@&U^PrnS!sg~$TDtR$#!XKUkcMOi*1V7y7vtuS)5B*d3>5cRCaK(rvl zmd6uwq*#SOlo8@bJ4dDNcbQfbU`h!y_364WA)ZP^WLiZ)G$X`}ItXDE15rYV zd36wJRtXTrKC79k;WCAc*T2MHL|Km15jf3ijx0?Hv5IU>yzWU>DG*J3mRIqbt(wKQ zP!)kf&_7Y9Y{M!8eWA~4q3CzVOR2=<6lmgaOhv4zlf`SbM3%76YNfK=O=_VjMwy1j z{t)GOxlWE$t2J_%git0kE^Py=4G@idR$IkNnN1s6>4J}4Kv_;dn^?gVs~xg5B*g4G z4U%rP2ciKXzO9oZ$0`RRpAetaL8Mt7fXE}nTXhi0R>#mKZhw8kyiy0_vpOM5E+N*` zL3pjsK;#f&ULAzT>HD1&t3nM5_X ztR7&Ch4m)v%Yyg*c*9YR81LTFRroPjpG-#;n&s?1yJ7tas}apJZ(z z40gJNb@2%g9)F!td}eQO#0KAOp_GpJAbBBJuy*GzoPtsD6!k442Z7MyxG(xYC+UW z%Cm~z`fI52+qxON5Ui?h1}~fflK14FDHokVNlu%gh+k9^+lH(nkc|AVsk+sIsO3GC zih7Hpw!N5&KoY1QB-U0fh&*Q6{Snstia2hD38cD*<2zOaJXmbPW)tQUMQpom71*Z` z)^AD<8`L47{N5Brd_)n)ePr@`XDBghK^SB3dl&v(v+K91tn&T2B&6dMx|C;+p{sks zk~&Blxwb>QSRouEAHcw+9--{g6os~6U8ASJg$1d>39eX*DvC#0fiM=pjz>_8o+g)pg zLON>@(y|izbpksh2(1EbQ3Gh*yyYO}8;rH}E_V1!&RL1~NvHqFcAPcw?ulbz#|q_4 z&N?S8{gG;uu4*Gsa`wG9zStr?&_dH$sF4a<@}=L#u&gU$-rZ__3}Z!f!}xx^*pcZT zcFMc8zD^x{@XnMGQ!1Q?i_--cb|O%T_)d z`z)Q^!|bgQH5VK4tr6OYpOVQMC+p3Rbf3I7#@hVVb~91`lF6{&Ec<8SBFbU;YZe>P z)Ry#YyZkez;E>i#Hd%17Twagm#AG)6)NtxXIh?wIckJGL)|Kk-Z^cfYjRy6Xd9B&R z*zC=$gvBP9>^kPqxf6KdoIyfQkJ@zD`uR_Et%+Z~BKEg`&o8Hh%^(3EYR<6lBzH7t zp-j8>YIiG*wtlda6#Jt&8+cY$*vOG1%GgA9I<}{bb^Z^vjiI(j`M<5=OYPWp?2yYD z$htH=x@`dx+%gFja7qEK?D=ua?5%$)<W8F8-N_KsiV+dpzSf5H9&CbXoiz*xp*}C{$csrXojD z(-cQ+>d23;W4*kusU-GgWYY=kJpSx*dCLUWyuKrfWBUqdXD!*9wp?zUfK8>xm&s2l z{8#m&i3lEDCc_h1Q5UC*POdDS>xhyzIp~0KkdGbs#SFXnIN4x0X<4vLPM^q{^Ig?) z?L?ME;WlFY+H(2TMAn2oDT9+(C-zuuz$DaDC$nPfu4kCB7R4st$YKo0wl}k(!oGVz z+go&a$IKl1lZlpl^j6k}OvHzi)mZv@GP~1hC4V)Q-RNns)M+!_ z6a4Vl;>{SlZ5FQXc^4@XnmW&)jdrYU^oM4%$Gbb!TIFs!UxLb4(Vibv?hrdzeVDY8 zusBV6WSN|LFKgU#xhjF#&E}?sTbd>M;3UZr&&dz>W6?7C-o5Nn)VNg@YsVKXi;byb z6AX_w=<1Xq_QO1uM`{Q}*-YogOX94B!AescoHMz08q1MyM$y=ZV^CqaovoFewuoKq zwAvYYW8W=e5u6~)?;m6#Ir%{rK>VHu8P3oN^kAZqNI0zUnSL0F$Ct1$=(aCm(_yO5 zU8)TC)TJ2Pu+U4<_kNA-UCM4qVTa_PCou94$&{7s?!40^a5#&u0hyU*D%}qh?MVF%0wFtIJ%wp3}ij`DfS^aHhG%ejpCkpn&l#R z<7qY$LGda!l`WS~tzuVWTF7{YRgl#g_AHFwVfoy%47Z8o_s_zaIxOq2R(0sQnw2;= za6G1=E)yRTubGnUg$8gi#= zrwU@98~V=^!%J%tCWgdTb~`pb{khtSA(4P4hR2?RNrXCnt6>|QHcuSjNfj^+G%>uh zj#WB+DRE+eTM@hRc?@PSnZBNx`kdWo$<|k~JlS&ubITp;G4UUk88KG%pFB1?H>4i+ zdW8$y>tZguHy`3IcT} z+>})A68#|yHg+TCqp}2N(wP>WK}P2TRy#WS-06x~@4JGhW4m5v$u!XRzWSHmPYmAA zdBRazqE<+nM3vC%=_ByV#`UZQr3w zS=HakdAnGCZK`G$yNnU6bdjCGFyq^5WfWTy1LmZg!II{!VV$!z%nB$_zY5C5l?0P7~`ivcQ z6nG}f9tWU1QqQ5!+3LTzzelaR>I<;K^&@HileL58xagnk5-54?KiLJD8y@XVt ze|ZNh+GuL4*pgq^D&d(&y}>1y4RDtQ);yw=9EA4JYoaE=(^0zc@#!KH%)cJC7`k(9gL^|&nr`sfZW}aD*bsF|?qU0+ztWIr~ zYFM4x%+s(swV6(^TE(1{XpDUMP$sw7G5J|0e^KmQa<)3`q<|PzX6vLOs;F(a&t5DBV#k`t-L8j~m?Z56A??CbtE5zWB-5zIMF8 z({Y8O+N>?P#VnV#E)jxC`KX*+8|qX;Z=O=2Kh=C-hge3ecXkYF#dil=IO>e z@ytUpD%@sk(nZ=6F?m4`epUYBCt>{}WiFR>XCzg)7{^o$%|)zDE9HAVcvFt>w;sHd z!jhgmLgD>A(I5yn^yIft*v8^-r8w^8$rUoG7w?s~cpGeKnCq`$y0}!5mTnJhlUMda z!_C?u=P$#c`k*&Y%fdugh%J*TF06ajJpmJKHLzC7?Y;Q;M7}-F;jLR&5#LnqVlcam zCdfSx6McGcJ1GsbChpm1Qn|pI5o_hsa``#@9A`%hRvf#;F6!D<+vLe}c$*CEoDs}| zdCymfZoPSZA{g46H$Hn!zy&qAzjxi5H1XC-FDiH$41MbgKCt7-7oq0~ zi~8_LrP8rhcxPMTSQx|ccz|?9z606~T&S*{HIVnD@0J!4p4OD4G@z2elvzm;Oof)l z5h6KaEnY#oo<*)ra3AR#yo&ie)zm_M*$4dv?V8q?x5`{Rm3TI{^HC18=tX^b>r8u= z&84!)-(l9$ZSujsya`{iLvHTNM?0&ggfAw1*biita(S93M;7$wh4Pkud|W^`9WAM` z-6W6qUmIDrPf{RDcs7eH)0fdO$5J}-TuzY@~hTR8e5GjT*j^6o+H3&4uTnf=c~z* zt{#s$=Ypa9!uqzhH_$C9x@HNLuS1uy_RCE}`TXoSIstWTsvJ9v-$8s15949BL3)St z=h;g6>To_h>Ci)@k~Px2gulQ~JS;!HgnyheWgdDpJj?x4qf^Ic#@@e_FXm0BF90yE z3t?tj3+q7KW|FmHL2SVld^u})f;a>}wH23cX21+(Scfze+fh6_3u)0m>_tC*|6UNA z>1uav@<=|A@2!&CM)LD>w^g0N#Ud^ns$y+M@y3Fmm?8gu6)*5DoLQTH%2lv#i)YGb zui_W*=`&+u4DZG|ZXgP)8?u`tMd-2&M+ z!tX=p`Y6JCpHtgmZ?T<63}O19=@1z)$3R=Vt4YPV)~4kIJ!tEMTg4G;mb`o%&tVVB zo5%6uv$o#R<+AyB-blVTj%TDECk`|~O6hz}o*2hNS!ZkVg|gXrKF#hqv|jvbJm!(Z zGU;00KVg_bYI)hUyuQ6{6km&IEp1X_?|e8GMJ^Ve_YTxm&Q0u^*+mv33P>)3To{vg zFmHz}o5bt4oTpHJe0rHOB%GYzK`4Of z52cvNu+nsK-qnTC)##*hUcEzZx}LX4Mgb6i=??kyBz|p=ccw#hI#LjmEZkLR*f8`7 zRj!r?dj7Za{Qdg>fAaJsN%#%Cuibh%a?}lcP<&E<;|AWWvn`{Wu2~2C_?*r_*3EnkLwI%pKOaE<0$c%Rc zx%Nh$nRS-VcWS5HcOwr+R=kap|4nCJMfl*O_E7ydQtYo~WOqkf3hR(!p?g|*)tmpm zykl?5$8O@S(MvzNiT8xpntL-Z$oQUIhyWw^LHEKn@5uf)^A4fksH>A}WQ4I+#e5mY zY@vt+&EOzgelu@6K-nkq(xw;|b98IsKsWfCa!w3*1iUAf!N%Z#i^?!@!QEL1V}enx zG%b<8lxhFNPw}OzWW8H3P3SO39=wHL14nt_t#F$&SFOUd6`vkbUlGjbfvg^g$ zc=cJ)F`&JyW0lJ3_|n*1wN5=vyc`I97o{Q2*UACV9D6#p`gYD3-?U6Vl;>_L_uj!v zQjhD7^JO|usuXw1v?8NS?*78p1ErH$`N<+?A)5U zgkgGvCxAT|e2Hwjg7?YWreNj1d$xQSIrzG{a@Q1|%jU_WQ+P=&yYHv+rn1LWp3LUU zep7kNI+#59z*J~@f!sWm7uUgbry8}siw|c}S#=i{;j?DSsiQqva>rdfHFKt_74F)o z`rwbX(ne#~=3#4Bj&(s^p?hCJ)Tu&1B!1 z93MCn5WXXwe znwyffSDI|Cl4-MeA9g?voyEIk?0$698F*}4&Eg^X<}3_6dxA4{?gj{^$dj{pul!j$ z+j)RMPU0{KQEZXmn~zDeuu7kJ4^PTa={kig=3%+&9_`OvHk)tAw5f;}<~HK9_%2y; zFIG2yF8=tVa(NYezyou5axx8gYv0tZa?&-P47urE-Zl5YU&tjht9X;Yk|#r6QN>%F zrLwbRbrr8h=d|YVn{-e@MK?N&WbiIC=E6VND=(YNd%#nFXfD4h<=8SB1+A>5)zW_- z@1pN+_NH9^w+g!iQm|uqaPk#(U+ ziDDJEP+q-^hP+yV4=g#POgn}U)~6;qs9MgMa5u1bjrF+WaA9HZuIAnTd-B-0YJQaE z&YD9NbR2tJIJOx6pR9_FdxWR6W-IHUxf!I#7@Uci(*l1nQCPF)$R{4XWH*50|XKUVUg ze9RhR6-vRqB@|Jwr3*{BYKGH5Ywe#9?^Y?qbALkYT&WOhs;I;2O^tvNd--Xe6L>zJ z)B3DN9()?R828MSr=Es}Hq4FnT*Z5`^b@wHRL@L-_$jy-7hCWQzm)N9ugK%i@>aRk z#H3E$8G6IKxw7MGKA2CLEAL*-2gLVfj?R@Iujc*uifY+p4L^@hSuXpp#m@D^YxtA= zKy?g>FH>S$wu>yrH?EZXc8EN_VWs?ehv-AywEJ74p_z+*M^lXs5KBYDpk;hTfLIGx z$jZa+kj(LjSNXjA<#)-VM6x8&lb=|q6XO#TA5mhjjp>z)#0ER@3?){mMBHQ+V|1v6 z0=E_}lp}qjk-W+)@*KOBfSzKbpF2DHSXAf!1L$ODy(8yki#$2OC(`7g6p`n5;8dvG z?Gf1;_W*EbDRGU>^6=TwD{ORI-&u1nR_IP=;fs&lRYl)Ed&Xl6G&;|JR=)4s=&^pW zu!}mhf~~}A>Pd_ywaMfhP!aU32UN6`6r3Z=1Jngp#O@5>A*H6PXab0PgLqcN+E@n? zgwfc!Oim7phJ63B*n>gQ)G%vj4a#h!I zq5Hvy{sg7VqVGR0du54sgS10T8*%_v4p7dnwwG9`aa;lf8|OTT+e>=drUUd#WmZ8( zxpAH7kbEsmjODAUW%F#&Iq7+FTUDi<88_Z9&&d&$6n4!OP2`t3VhDdt-kGQTOE>fD4Y6|CW|{T|U#TJ+-{8+7 z*Mv9uQ;FyP_;{JVlqIcT+Y%cI?Y3cW>2U1t+xW}L?15O;9^S#g#>U_e_`~oDj(vd5 zxWls3hx|#*_g{R--K@yYoOnq9NBYE(PWLnOXnUXTlh)hI^W(>oXC6KF{0DaF56hnW z_*?|L_Hnw=_uW3e0>Q$M`8}btl#*84w73Z=yYJ^Up#EV$k2-oulnI9oa>*xrH%P{R zs)8X0upNI`j{S@`vR{Y6loV9gapV)9p@xU0O)~iazt_OWzk}GWrlpm7&?ZkIPj#3n`_c6^wC2paF zlXsPkdG%NV_snk+$oo;kA-~;D-1kia`JqIL+;0=e2T=k-T=8uJ`MN}kqlt+I-|4(9 zze^yWZUdF<8xj-8?8G05JEcq9{%gw4|dr$F94+h~kTs(?kz7XYXm)s`A z?KqHEPrt}dx+=4D=PH0gGQ#rm3}jT&_JTQM=b6*G_*-bZH>u<`Bm;fy-;YL{^Fms_j8k)ej-<~&_9 zIuvVF2fy_jxwfIWtZ}7gRGPr3v<~5Z#puEmScY zHms>QpR=`cRk0{S@P4r%2# zqT8~o%S3Lr!mBe~Uta`xp9>T0bomKzVDWPqr2A4fYJtu=J+`HV7|z%lSopw3ABh+tx|_}$kc9P06mBlyQG`Qj!%+jj)}Xwi>ur%S1p2) zoX>SC0xePU*1DiFfTk}76`rJ-F7tbf?hRg06!8zPh+@A3k~j4hA@-=0y+y;8A1Y}4 z6S*P~ep?q52IwwHGSHo+FNpgIMPYvqR1q*iF-uVZB)e6B;sHew|E!D3Uqlp2LP->l zD2n(M@aCZSHBRw;1@;XN%eH;QG`?q1Y(pQxSYJm}ecw0j++Ty8 zS~%c-8hm2WqJAQ)(y5Sx?W3_yEgW!#1}o9=`Hchat-;Gwozvprb2NC30{i3OUK+en zfio4@iw8gOojqDjKGaX-X=98}<>8R{Ri)%2xuc&5d-tfy(GdKppRl}6U2!3zzv%9D z1eh*oQqn1Sx_q0GPJz>9<^UvjtMc=}>XHFy|3~B<14KjbR|-nUQjZQ$I{)VY(Tqc` zw1J{A|5eKJfue|iCFKE!8c0z425f?e3!yyQvO2WA}JdTLO4rGi^99495YBf z#*ZwLfx$@cUnJWM7A+b&O{p)vCE?5Hc+K}Ll6MRiJ^0o|@|D4&kiW1<9-?s7BAIe7 z!bcX#Run$4NM3xd7~at7X-?bZ)UpEU+vke<9i0*6kl-r5qi1KFI4FE-?4Te(!QYVE z>FkcI_7B^Sje{7qcp#$fbRx;W-~HE zctlJ3E)|y}cjQu4K-Hx%yNBhS{1y3#H8cCj<8O%)tFOQmb6BR2P=>O-4u*~p z+nFX^-hok}JzLBtbZ3wraI|L4kQ;9n_5E*imRzYOW4zQkWLjorOZIs3-%w|Iyo2GW z&V+|jogBu?KsZgXSOjlL8rF(rdN_z){e&`bl3=>+Z0eJ25P5h{XpkD|E0ae1rVbuyK z%2}Mh^=HL=&dyZK&@=RM098LzFDlo6)XO18XEJ}Sm!$BY^>T>L2mY*==-Q5%a!oH! zGW{_wP&EI~8p4MO^pV2Z6%^ZjGrY`ps~13Dc%wxd27xEapy$~N7@sWZvM!+23QAvN zesddaw3BnX>@ZoJ$Ez2}dnSvP^_{%hRtVezN}P;Bx&qR42c%EedHPLrqyY<4N`aYv)Re(~j?f|7Uo;T$o-`=d&t60*l!aXDW; zPcEA)8t{ka$(JdV^W?{KMN#)C6$nq?ZX$XLCf9k0u7#EmK9GaOdoVc;w$!=@S3>Ut zuW9q-sM8DEzh<->&N@#Gxv$2A>W;W!h0=g4ns4pHmA6jc)H8M7Q|c1 z!M5;O2k(W0XDX$h3_F0Z3PDae~PQxfrC6;#BP;`2oLxx9BoIc}#@X4K{ zEeF()6iG!oo}a)ACzhjbhbrHvdvHRx$@A&{XL*Ll<*)BU1M0?ia_0KtE$)EeS~y+A z0cGP$7?rVBWAHA}u^KE8O>pb9*8n`5^x5JwEQDK=V)sSN4Ei$&1qEQM(u>1@f- zhsgdF;x=de(nk>?ytfQE&Al);n68(v5cT691tjLCOZ~f_sLf3u1Jv2OqSeh+E5+Ad z@8jxGGAt0}u;;`L{P73nJI{$O2xGn)(VfNDIm&RWtwLQsI8X%7PRQkJu&q90o!IUO ztBRxRkdCnAs9f{BXwMf^$?E56*&CIuN}Xlz+6VQT_`Un(?+S@M?LPK`IceX$)Etdu zXuZhg(-+B+Q=O&sFY`6|sRwF#B^_7q3WPQJ#(U)}8&TT7?xg~jX=qV-$#yqrS%jXf_%QdqT4hwIj9_!l*D8NoL{r`zGP z=jA6zC#_IS8fuQyRl?MX*J_A;NGE2HPiHrM(sYHf8}{)U-Hnc}r7(;CBUirtGIl%; z#eREP+{iKuBUUmwLLTx0b0eleLN0t&4A*-;p#U(M5qM56qCt>m2INn#imrUsY1wv@ z=+6T3j!mLR(}4D_0%l8DoXB{r#VlfNJ-rE)-1)G4XS2vHNCl-gg2yclNFKrv z^5Zf%@~2XeDvj5$xt1!gMnICj{hAmDNjq&3Et{R4E1j`8Um{@(Pu&iZ3$}<-I&0Xp zMYIC9@3)8sxU!VG6>o2+#+q)$!Xr7=gcK-NPI+A%=38HvSHB_pr*EU{w0K5SSra+| zdR=aLLtH0PF}&NoDaxEf_*}99 z2hS5a$*oj$XBTQ`Hb(8_ylq&XLXUg4i3(qUhE8$_<31om+eP0}B{4KbTK8bjlw4oi z9`c@~i+&1I%m+bx^R^4qS>SoBY8ENPq6cRFBM}{Ps-e8D$neN1Z;2fK&wC_$8#DLU zb~ybBo&NqFnbroDvHA*6GJg$;ogn&26A8S!bN@U~KL5lfUj4K@Fx?^k?H)OQjGaib zAI6dMukhs1b*oyQ^JeRU7TG}B%}m;^Ui?i{!!>)Ide0<>)>-Hw&UwCkHN$g(I5uUo zy5tc%wo6=Ue{oeHHx+v>szvSJBd*8e+%Lq=`w%~^!cT0Fo%doxci{{2?!BT}d^`2z zdS&k2z703B+vMEVtbTOF%^t7GFho+_E)nbUt>_`nIibFz!14&YbHIcz#0DLPEcz7M z!NjA)F0_Jr3f5&E(?4Bs5msne4umd7SRAo}v6sIWGZ}iRko|rXosyY(8LbU3lS7X3 zev8%$L+<)fG^#9UWQM}6Teogp*ravqrmb5SwJt_V|0`+MycDsr)(t3~pVzv6>)h5k z*;$zx>1n}wset)?DPF=QCwbfr4XnOht$pTjl5rUR^bNEzhvNlXM8hM^VLGOPYK;ky z-1)OeF0MzC<=eWmFBttL;nf011Ai)dfj z6sz&Lg8SlGLnI7RrO_u4#%qR&$i|7t5Ity^kTaZ!H1VimA~KYSY=rGQ2PqHzBGRk^ zxb+T%I&-5$@`iZT@XR(ZP}K3pD^Pl6rs{o86`9{un$5GDPjkssJqOpc(fu08Rwps1 z;d?*|@?-=7wt@2HZU3rm^*mYccM(c{jyh6BAWy#iFVQ|tcPmD3B#tsiULVMp8NZ6c zE=Z1~HCK3~1@+|`>uYeX0yonK zD!G)~uX5w5byOppUQvv8E9UH6S@N4GZ<9xIm}8u7$>^O%B{g1U98O=b zJ61zS`v!95{NKc|q}6KdnKH6*=ScW!?{9W^(sqvnQ6&E<}o(R4~+JYtEu7pl4!DpfYDtJ@oJqqL!X z^mpup>%3Ux$#;-+#&Euz8ZdGj;L*#G7H(%XKsD5_p};_@5FlB4T6AorvvYdv@+^im zPxZ8L+)zL}S62i`pJlEKlz0~?@p9#c)1p--Dcr)DX?>JRZ{1T=o;oe+_fTquAf(TB zwmww|km7cs;&!3tM(i(AfQxMQ7ycnyCZYL9RRnV6>_0@)%5}P(wW*=EQQ|5705{b69gyIq!-TfE@`aL&-))r8yEJpr*)+4OL{RzZ zVWnWS#ahg}LOcW=LnA?rOgckoq&Z4$$c|FqNjO(7x6Zl(S|07HbRSb>Hk8sN+sK9E!zks zn;1&6!I{3eR0%VhR%tqRg@U`Ga z%*<#j@9;L6v7p5vi zjj>@K9)<2QivZ)w`E(VU1e$`VHHAfj+IFHs_}n>U*9K7xCAvC6)}T+Il96>$(LAl0 zJP4+(>PQ_r8Y&ErvOIml5EkYS0}%1+CWXdx($YBayH4@EGh#9DV*~TmBm|w{Bmtmn z9T0RySyZ-BsrX497iR)Zpg)2@sD5e+8c`obAk;rSrEFnEpa2BwSH%#Tq%jca^hli! z+LL4f5Xzc5HP@+xb{q#5$(4%7C=e_25_GIRd;!&bu=?m6u=Z4GW$z$?nn?kmnUt(O zri3w?J+ZW<(k8twUqLS&kFlLLRgp?n1MS7xHPJFuU|vAO!6+$BjX{+3i%~$uLBV|k z4dbQSRRxIHYZsjAq+{c*uLzA-Sxq&-;UL<|9cNbBdbTUB1_F-}Z(I^ld6V z42B3)^O>U{i&Is*Ku3j6v2{3}DH9DCm0#Ci>ugk=(K8;7j#576GRUImGIZuL*LjXk z5T(vcUmuTBz3p0^7*~PrMoptSx9aZjKb%>QraP?eHIqxJZmk^6s-XbQ(iqS);Yb}i zV*oQD1kf`fMzER*0njs{5^f01Kx8+{Ttnox)55IOo)J7W+=!VwcQ ztP2lKV_rvNs^vHbXZ_+==^ucPf}q|xv5zy)x8{UAL}mXNMT zFH$4TXe|I$2IatdMx)LOJWMqbyqrFPpgG(e)+dlkk5nh*4$#xo32FSyjvP?WF!Sj5 zcHkzEU#s#5bbg;dpoR1+A#=5N>L(V7-f4h*Ja@$mat_dED6tCEECUfKLUC!e%OC+}WqXMk0$+VMzICAvF&0t0?ki zpK+e!fqLcS6r+i=p76?*DMsyj0`*r#q{wemjMC!2R7aU>m?~lziolN^$^CvKFB4@e zFEbexvc1e?Inie{@))EeL$(YU+387jwc_?C$uGS|VK&}zN>Dvfp*l%*L{F|GdU9RS zYqg_9|2Nu66g^psj>47bNwuOUY0(9VF0|;Lx}qC?kM4kO)d8Jd2VDPyH*Su-o?coQ zW*ya&yK+m=#5x&{(1=zZ=x}>m2c&v`Cp>{}tHFcCk$LD^ID8RUH0=o6W;c8dCMY<< zV|8D{#Wzr1T?sE3OC)&1@QTR_k&_Ec3Tutw9=cgMx#L6s0|)mO^BOq0pczXvsGy5t zuF$QZfe{JahX0d6bG47=?nvkwI2u=Le|WO?hi}#XFnnI}hi@Z)xE(hEC5ISLa){wU z!yz_r(f;tw+8>4+isxa>|Iz;N%`z|9D1ajbd#Jol=>LE13F8RUzSNSi_)NO0@r#C;ab4tJIF(Sv+CvLSI}fTC!G>nvt)zXJmX#fK5#JzmCg; z_OqRzF*F&e3F+!ofo*H@w9Jv@31eKV7{iG^V)eA)7;F16W2I#nIeH<72_qTQ<8&8v zW+i)`i=1tmvU{sDHzBJNrD-ae#&zN}MI(=l8ZHG~0}uEb6TP1<=HtgrBjz}aA$g4# z76hFby;RQi8rf|`-C?L6!=Ocx&%DgMlB!1&>lGH8gn^3=eF28)vY>HuH{3Cyxhs{7 z6AHmD>pv8P(l|45pk=O11v#Jok{u_?EE&o%vO+^N6$d0+y?;Yl&#MLI$XRJdQD&|x z=-fmG*jXV4gKKH?;tU4W0`uj+z#yrCVlY5fq#0q^Ki%+U7sRRi)lxT-k#ta-in@KExoN= zmWk5aDe5-zYoh*+sLSKjt!k+|$Y2(zJ1XiH^3qHr94f0tcaG;Ntp#?GGbm42m8Y3} z8+llXJf3M}`ipC+df;JiqbM1xKI%I7XgbSiEC*#9zMR);`%Ymk{T6v$wo#nCRnv#% zI?#thwfHw`bD6c!H{}c)utt%;E$7t}W<<8XOuQ=_nQH0C(xsDF3}Wad>E-Rc*caVUnpsahtrCtymvFnNyQ zVVXkkWl?;X!|ei#-4GgWy37kZyKY3_rdbl#kZ5{zndf(QWkS5lpYhPF=)$NJXn=k} z+%AxA%25>6)rX3xtA?N~Get+CPSdBe>_gPAqcCBn`o%l?(dvkqs-YhtT2DtmL=>OL zf_v|Ge1(Xp-(vzd-{iI~lGtaHRH)v-sW4Npn0y2=0wcD=~#BeJWt26BIXdpU@3;<*1|CQD34?P zZE2N3zv05Q*83pyTkyfGFw;OJUZj0R*7t%g_ z!LL*)Pukg68%nK5$MUKeyi0w}MD>ZKy6 z0Jh#-CbZc?-q03=2$N_U;t&sNE_&=Y+SCT)__RhdsANO-XizZv4j9SFVMi+98t8(lSR zfsd|^tF0AWKRK^34wp|C8YxLiT)&XV8yn?zhD=_YLEZd=qKC;QO^n{PRh`*Hjg}{y z7=`-FS{fw7JuD>+n@6V-sDjkvz&<(%f=Q7j`uGP0iMh|*aVfn3|Y&=0~WaDK8`)*4|PvfMm ziL`5r6KIo5K)b|dxiXRL#*zfGw}@=>qrR^E-OlBu%+jb4Rv@a$%X#F)s9>G9$l{{TooQg#L(C zMMWTrXFgOk__Zv+9drI|6@8@GA!$=pf~5DgNRV__Z%;NqP$f^dIFqbPg{QWKx{mdv z$z{z9H{X3WmZuYGTeWc;a$+kZoj-d9Eo<)?MzuKD!PAiAWsN;;uy(kjwUHc8R3#)* zu$QW)D4Q|`yQ2x|Ea`582GPhY*`_wpSYF$vmS+kU)Ct)dr@-+~LLB4Rg(u=LuPK1Z&<0pV@Xjtw zQR+22m8qO74j5DPO4w1w4f8H;FVIX)kHsh$hUua0vUNM-?|kWcIlGbcGvGbBaKjeZF&>I2X|6JAH$W3=1&HDpFRHT!R~<9u@p9w*)`!S? zgz=>$Y(qUWHU4CS<6yvw8fC81Fn*l@l`3=HC~xRs3}_tB=Eh?ZZrsZ&as5Oydk2>b z0Hlsjl?OT)jcoqNjVUPAOf%E%Cn0Fk#GX!OSp9|)V)_S_pomr-jms;QZ(uDUmQ?CZ z;SBu9I9dfyR_2FN2{a5psSd?oh@VtPa9${zf8t(AXH32PN;-}>`*fbvO>)%$dqF{87(pwDmMtqDzgN%SL~8gH_InG z8E>%}v70&@V_2!al^H~9&7sSO<@|kgM5J(Mt{e|1P^ScpAk(KxHV*rXa3Xh7x|&Du zcN32USJJy9c*h1CYzmA>xH&uon#AzAZi%So9T4^EYn5Il3f;n0yf>*bI=ePdxf%Lt zRI{XkAcSe7@>IOEkV2|(n^cUVtPaoCQRn=A1)!oPAv4|h35q1J1?s>{v3lu`$KyFCB#U<(KS5M@$MO@T1#CKM29m-X%1^X0lHGMApuA(w z;Y0``Tw6gI3(+8nXhevxzXNG78j)CssOu2Xhz|s3W5D2K@Dq`xPF{MN-Fc`AyN#TH z6y>HO(W(7XB%`!zIFe!x2YS<4&mRJd$d0s^kDq2AdvtqDZ}@_(U^vv0e$-ac2&Lm? zCrus^-g=8&QW5wwA}6}-Ww+DqoAd$luFfczo^I#G^z9HQLLj$8G1MI9Hq(hO+9QS? z)@V9bfy$zMykZW8aoCAt*5(sq#AAM3%pQ=}_z6So;wJ+_7(c0y?D(-GUFj!;`2qhC zF2kv)+PkhS{G%LxhCQU9hifTY2E{FhC-mmgqBA(L@xbju$BSETPuj3+xjpET56kUJwmxFt<(;GK9);b! z)Z3$BFV$<~7O^AM%D)YoO`ex~yBn1oM%i65gVS`e{A`qceY+As(++>#phxB4H&tFa z+U}+G08H{0Hq52)l=m!lBk-mVL}By_t*1Ar4_-Qor$;j7v!kIatvD#Z9Bm(keHHFG zcIOWMZVusQ>1_*X@{DuruA&cU32T|>z?kVHXPs~VS{^vZ4x?Dxb0PWr$g|J2FQrWy zkDkls4$Vw$cPT3{&rLra1eqr7^K5shN~CVO;rz%c&{&`|wIkV7X-2xy?+Tm~O~1H| z42Mld5aNuNeYhD$UVffkq%|XSG&SDQ50g5&?VwzJo;}Rmthy#UpKs@7s*auNa>wNG z^X<;<`nVmiHKr33wvLfB?p_(??BjMuasiSHBWcvtn*22Ar}>!0$@83g`)z8K#|z`B zkG%d|`vmHE)dk$~w=S^h99t&dye!o}6DEE;tqr^Bc2xanmPLw7h6 z9p>AK@)J3b)Cc2?%{h^-_(RALoe2_)gz=XRxS6?$tKIfx-Yq29BiY3Ia`A=s14es! zxo4sa z@t%V{+shZOwex`b$=BLlt@h|wk6*F^aV37@_pRl-sdCjXp_G)!*M4cA+&xoqX&=R< zU{B)GBJxI9wEc#dwT(RL68nUK5$sZh;B@mL%5rhJ8$p_fp&$hkGwD8zAZXbti)Lem z8?fTy!H}CRAGyRXYUBE{L%w^7eQz7M^;jvl|dlusPLEw;$|Z9 z&TH&}1Gsaj>d*v0C8=9KUSreQO9QX9BZ*@eS|7Ky*V=Rn1jem%jLjz3cN8(EuW-!~ zv|T7X$hwFo1R51k5KwH=B+GbL%6LW@b)VFA&%TWZsveVmn8l4pkJxmUM+GsAZ7rO# zN326Ol%A{xn(!9V2Jwu<)Nl1N?Lk((ftIoqnn4bj59mk;kU}qfB8?wX9Saz}tdM$v zW2+r2T%^}PwXdWJjSrI;h?^botWA*hcy>WR{qaLO5h#(Z13pJbSq9|FW~;=vM#{&L zb&?&!v;@0_%FTop!*isvPXHK^mPdM6FQ*EfSXp1h3Y(kWRWFt7AjT zF!s;_Y1SqUFw&N0ebQ3-a~JiwP<<|tYp$~^kLH6U>Ij)wQ#%Sj>&X1az?j25;9>`R zBM7$e9<5(^b+5^LueZCH8^*~c*V{e%s8Oiru>c8%zom?>SsI#YQ1}{+v_^Eb{OWqU zA9)(Ol-VWu)ijXX_&Z2I+oD9oi=LF%l-b8C;MhX_cYu4Z%Vq)oXo&pY)YC=f@ z4vVpbALzs+$eN`caxAc-gRhv1)lSRb++cSWL^;)R>J6$x&)(p72n0juqz+ZmL!Ii7 zd80kJZ33NW7I-}MMms-8B^pS<-qp!;iH*1KX?f?3_6T$S>+-o9p}gSts~hcG%=)L~ z<-f6Aa{P?_jeV)__yIrDt&3m&#vXZ`UJEG(u|ckP$4~rj2CzRJrD72;5Gls*W(c@~ zZo0`n6Q^-+xXB)6?s$R@WF|-P$eZn69cteMXo`6X&_!I5S1-rjZ1Xi~Pu*<)KK4MS zA!6M~WRWp;Lp0Q#pG`{hP#TEH4M5#0fP|6cN8o){Bzl8QO1P1 zK^9AhIy1@zSy6Np<$_J;-4iiAgTx_ZnmO`xI+GtA!&(ZOAWlAo{uvdeYe7+lR!`}~ z{qLDR=?m?|#qSv|j&4>nxoR%gv6*ry$>k6y6=-fb7t3s>n3G(LyugW}BhNclqf?qGCz8t{lrd0F zd*PcY2AT~8m zW(11G)LBRYFduEP9Jnpe9IZ^lk~G08*fAG>K<<+lAs;lm^en zz*Ef55rUqW7IO7{B^=043ydO>WHp{>H$S+8_Lzf?e5Hdv5GZ`K6p~Z%M1ld63%N}6 z%$I9IS~Z>dnEvW$I;5*ls8|V#55_M<(IxnSWI7K&+sASi$SRVykU%beF2WlE1QN*8 zlOzO(>p?u3=5PXEtr@Z~XNxk7Q^d<5AxTt%{s?kv7>+I_S#kc8e}9V-P5XA$mBS~t z8~?`5nR2WE`1TAz)5~%PS>$PgJfzhjz)h7Ra>jqem8VGOdp^{+>Cg#c=tiPbUIjr$ zL8(-BFaSdr@kF!UaDK|w*r8UEmK415PYK9ZC_8=J%&%1hxeqfcz{s5vKcSv7#>%|f zaX_uNnf~>2zM5hb`BP5#h6sfK1D7;@i#8)0d!Sfd<{5|HaqbjrMWg}?$__Bl5&nRS zZ46o%MkYMrhE^kzf%;N}sq%-`;fTi>9+u|>bzjpX*kG~H1%f=shEIQ(k zHr?gouQUG0jLOHi0{p^R9qEE!vUCAK#Kj*X&?8ZXt~7KHttXU*?uN#X-i(RF;1VPQ z2NHk`j?VORBm4;XxlS!o{7Nk(Tumz%rJ<|HX;~V&l2$cJLsw8gp%bg6j*G~7Nh==h z>1PxzXMlG|c_dD1OFt))%2;d;hlc<^!y*|m`W+l$-a9IiMsNM$I>1j!1UyD-9T2_6 zW_O&8h95T~=%+yOWv*HkAuk&e!*HnKCnMt0PiiDWKXxSQhWZ)zMv9;jU{yTijW@`L zNwdG<77dLVS@;e03ysBP&uG3hRBl8dJ)YPJD{HtqkVsZPAZ7 zU}Lj>L@3=R{Rr_^rym8ZF#QMytJRN%Sd!6?aOPF(NB9WV=|^{7&Q7BSwoEliMO<&p~e0i2APLf)XQ`CnJ^zsW?O>xIA_8 z;`JVytxVFfsSbUy+>sH|MT${%6sIpjjyWW8Su=;B0R6g(stx~0&7e4P*4pxsd+koY z*kDJ^EZYtlCTT&Xq33LzovP;eI=eKKiZ`-ii%tE8(<0eF*$tAK1IOr#2bQ(vbk%Qeqm2*h>Sr^hqVBa2K!_ zjM9>`8Y(NMB+6>iQVV)ZdXmmc^_gB8#UNPvt95cL>9eIi1H)XH45L6{TYVM!D;=g1 z_$t&_NA&_tbKI8WrBJ&LK1S2?as2=zDBZWf#>7C%u1WqAShX;~a64vL-IK<|5+Q}T z*t{E;;W7sGx(;%c@fn_UMC3`(=r9e_!9^)ll?B*QW(?Jwa(bZVjAm+%>8dmQ3nYXK zZyEl98VT}7J=H{*XfKM<-^}lNs=1CDjkGic`4GJgoSWQd)m%w3$bF^!Wo*9+9nM0T;MCq2^7+*;lynHuPJ*+vM%;Y5vKi#3B*oJZ6sS2LM#0@0x}F%h&niZP-ls_G!r z5h7xyMW)1-$OlR=kqjFIAkFYX8l7lHW~n*cH9<%P`FV7<6RsglYm=H1Qi2ZrJkyA> zZav+I3YK%26OF8hV-3fOmsv)NO8Ok{>dsK${!M=H+G3P?r$a;eJ0i;p07ts_6UCUJ zUy6D5Ik2`CGu ze0-vDL~~dH1JWe_Gr9`*4XiMV%|(+?Z<`eyWNKw)fYstz1DICeV_*OX8s+gw@qS+x zb-Yekm6YF%vD!8VM604owFtgVA?4A~z>?D9LAh@QNM=xIV2hGS61F|ih{80gY2gJl zmV`AZ9I)PK%An2qG>~|KvU7J?RvX*t&_Q(Q&>@69<~iQvIh)E6Z0v_iw?Wd1U{Yp# ztS!K{byOpoxO2$MhNiL8gWC>2G=Q5NVqhBu4myxG)?RDEn~4z|zpEYpEA^4SHv z$BL1GM&^V1=~_&02S4-*aXgJm=TH_(Pc4h5??HGWr`I-0kKqGXL?6P*1#+nYO3zJB zXLk;z<7kZ#IS^d@-z&7k8Dz}|mg%_`&wGRmFM!)NA6SB0;8x7_gVMb{+)@9i0#=<{-7dj^s z$1C&DCgfNSxo;Cx7M@RvxEi;ph1Ey4Kr{VDPs&<-|qmTqAk*kkK3FvPZ91=BhF(d}iu7-z~7UN^5Mh0n(@?m(w!J$zj zg0#S}wBuTK(5Qs@u@;a0AnQruKWls8*zLDPGBgphzhj#XFA=4-GpkMmT30oL2-j!IQ`dd(b8O3iC)F_sRc+!8;kf#as># zBcW!d5ld(gm}fD+Ld!>QwsIu)Mxr_Btr_TPdCg0h>-7!H`fWeqo%LEsp)JIhdI@kd zJ;O1xiGKAWU>!XpUoHLa!Kx_JLM*-#81T*RbDHH|&LY(+EWgEms2j}i~}p*cMtIua~QBz0G% zlxusI*8JJu3&o?h)8RWfY)leG(FmN}} ztiYMU6nNI65z|QcZzP|nkA6RueDvygzg^Vf9US{7f(x`c^iL`fcUNJcQ_=|*^7fGV z4lQq!U3N62QL2`Rup63hkG?0jCeWJU9cF777WgH|Yo*VXXYWCb*vfgNf|@5tNG|%b z9{e|K!9zo)D%Adnyv?vkXwMn4sFUNkwcuhfqv7JneF11 zuR0qzybKCB1`x8_Q2h?Cp!%pq^5Y>La#v#;(H5y$>I@Frw**k}6+=h98|n-_m4@Kx zIvJHwj;N3`R$Fm*AEb9e7Hd`oI#OStPj&CUm163is;N7uIBHNH)~l#ysg0Y+Gu{o@ocKX z_jnXDGF|opurWuPoS)p)OM%&0cnI#M+jTGua|X<8!{ZUf%`&zLpJHkR zq=}w>Oz`?WBnhB9<^W8Bo>7oNr_TCh+sN{T`G_w;##fccsN2ogPMk?!eJ6mMpiJH) zO};hW0T93(C=_Sxg4=0lKvKE^-G*%9_>R0TOg+u?COt7fq*&Bh zcy!q$7jrgVUz%)(gS-woLlCV9Z&Vpuh;0x76r>zDHfQ^^gUblXt#6GcQs@PN6ltS| z6kb(5qHdcQ#Dgb*3Dnx=8T|~bw0T-TD{@0&O>RE=l3ES@o2~kdc1@2ZT189oR&i1x z5YMgp+=>QCY-}4F7aN+kd}(ng{!N;l$sLjccgUPmuWpw-28YGX)Rym>;wSh-faO-a zxZI5dhkr(+q$C(l+4NaWkql$YB8EJFjP{rrbRFc3B!aHf_yyg5-91F6GX*erc! zLYE3Oo%SC#oz_Cr+;XgirV#`ZXc{UM568dT>o%=5%`LY{=*(xXY~(aPY~<3W=D-a4 zyy7n6+MCLo7c(}*|IohhcgX1Kmx%k zz5PoTN^SR18uf?G#1~gGWGgSdieCH(D_br^V!4;%o(&zw%0FxPH3sz^*TcEiaMVh)A zW;L2!2Pp&^6+n2!hBTDBzdexIqd6M~+k#QOneKoNE zI!Y+sf#CjUpStN=NdnlV7Nz9A@;4oP=m;DkfkN-y_ znT~}PEberKAS1Q-Kd}$v>e`&st`4`T)VFLXb_6Y9GN46Rz|f2?@0*1TWZV#9xN`tD z>`3+k!Apb%MtBclfWeAF2$kCblO$FxTUm?F$fnN;1K~mPzAB*}*`pPi7%G7xUO!}4wiVffWB)&G!rBt5f*Uo)1A!kmbwQ~;3P;2LDL_AnIM@$>J5x;t1Q#tKcQOlZ~ zZ>!U2aolXQVp`+V;8rNkAV0k*tmxFsjU}9kn26Mp0|+>2$pPtV{h8Muf-4TSa5dIC zI-GH<6SWbTIEmESNjmV20yXmZwW;}m_sRB5-IScm>bk0cW5N4b5NF+`?SnT5It)h+LFcB zR!C{)Qln*3g6FxBTT3~dOmKwF0>9<(7Vwlg9Mt?ipVWu^&@A?hZ>(9EG=B%fF~ad- zCuE@pplz#-@c}9NJ7P8~yuh7o>cyoxPJxfzPhm{)7$?x}hZ~Hb-BW;-2O23RFCH?$=dDD%X!q!-IqfiHbIgHX3Z8~LV zHd+p&IEHFLwIJMp9G{5CBqu%$3>YWmMF6au5y!WROegi7BNazxrr{9(?@Nrna?WZi zeojEpIj`@V_2A!kn0CCTQ<5HJCfThHTITkk?Zw+8n+sqygInT<#RL{Ct2kak+CE-2 z@PzNKy*{Y zX4r>?Qmi3sRPFHP5rGkaiqd(^6?lXIUK*Y3FO4=NSly#UUPs;Q#1~g_MlQ}R5mBu0 zaCC*{PdD{AO{cxgouD0BL($W}iOCs(CBA!Idn?s48SzsVm{c)Yz$KNEa2o4bn_b2Z zR2(^Cjh%~&k>YWtu%8b(dGoTOY6)#QO2v6Qx<4I-XSLzrt2V;M}R8J>wxlFM~;5Z%H!~Fk0hPzBu_OHw{RkqVMF2@l>ntm;aZ+Ucbw>LnRc&q{ zY?6bh0F&=|RUl#rf@kd*~ zT%<+t7sX#U{B=iK)0oMDq}xF$)N3Vj>aNJwetAvsj!q)As^wy5C| zIcg2B-}7dpY56b+#H8p5p&}#|P-N7#7$Iq96w_z0NAnt9F|k;3bYwuo>j{jrju%al zYo4^bLMVK}5s=w_P-F+N!GYwY_DI?`7rLOEqIJQpE}u^Cfx{3IM_jGf08L!)>Fb)5 zs@GpR*eQ)j2Y&bNYUz?G8zxT^d0}SsASI+Ld-z<^A!wuF?4_iP`VoF0aTu?ZFY&+I zh1a*(lj+jT%O~WMXVY|}{wK@r=075CYGe!(o|Q@47=^cvy6ENT7(yBrPJWk>;oode z=;!43IXN?)yQfO_vFWR$Jyf!fQ9rpzCC`p=n|bkC5J7^ikZxgqL;wXE$z)r0p{U3d zbO}EWx`UqrbOk?!$cCRTZV^`3dmt1x8>{U-u_Lh<&g5PQHAPSzkDiz9H$!I(N8Mul zgN-M8-5+bO#FT`Hx43J5fT>9#Ic6`!PVV{OjX%Be=-+C-4vo&trHk)O3bFk9#U~#| zuvb1tDqD?aoE8xB8Zb})P6`H5AGQu*(uMhpm_Dr6fnr$6&gLpTAK%LA$n!__WL)o< zOli@i+JltFK?I<*E_4;0>G?yYYV7Q~6yux4wZ#&I8%e4z?&616zpVl~(i_-u>7G&3 zo2|!9RhjD(Ra16wlWGkD$?OmVl-)9Ls}_zF(d4J9>_lYAASNJ$O&>s~^eru*6Wd$Z zf&n&rXlxeI{i&w6k!$Uw`L?55NPYJei-qO1Up#8!61-r|!T4qiWLQX%0UX*4lEIT7 zqlaNbd~+{q5DjBLExY-Pfu+KXP6=}IiO;v2Xem&wC}b4G{A`7BNOd-%M7rG0&zk4w zOfafnAqE+_$nu~(7Sy9SDf{E(X9Z6D3K4-F^ntjyR%P?0o}UX^VW6vYBs@t<8dGpq zk{r%R8d4@_WaYw?mm$jVVn(EEV#&%>hD)*(@SElXH-eHL1RNkoCT(gVDX=v{N1^-O zPs3vpjBS#Q$mXkvYI~nigOkokLeY+MNGrN>NY&V?F;5Bx6bs}{C@*@?$NBKB@X9eh zfm>9V2kOLv%U-absvX;ti4~Qj4W>!Lf9|W{fh9C@JW20)yTizUzcsMLk{B1NDG)v9 zj}76F7%D1g=Hsf4&j_HAzJl#wEd(HXN6A*ky+Jk5Op9m*Qh_~_E9sNLhzt~BFR^Nt zB5%~*iF6dOxIlkZfGMT-VYKGRALt;bs>F$+Fg7(dvOm+E$N)06E2sQ!c$ z-&!HJVi(reiF8Xr2T&z^eZ6sBIFI-cW6nk)+3|J&M7m30K{t%W!6AQhl(y7Zp5aF0 z;Q>~#WDr87v^Q>*qR#0&ro9}QK)?o#g~qu>dXXMID;%6OXk#X5338Lj3tIw-YLD^< z9TGjFKkEun36bp6L~6d)rE+FIPeR>CRcBgdI(2ahmhGrDW1QC0jba+3^iQ`$h@ypK7}6ts+(@JZOT+D45h-EbJ|NDii#ZZH-e5|^{ zkHDtLkkH4D+YiXdLS%e@Afp`+R01F-lrlBqzUg zYZf0q7mh@Ki%1i<^vJ;h#S8v?Dxa<4g^%%M$Z~L-e!?LES{0>z;b^5rXBTS2IRjKB zP{G@5q2CZMG}Ts+fi?j{-W$Kg55NMuDhT|2;`T_l;5H;DphK8bI?n&J-1Zo)JoE;5 z2S9oniMu9gR#fgYShLFDFp0T{Aoe5(0<<3>hz=l#wjhR%Acjm3LTm`h>!U~y&$ro#x^*?JnL56hn!_PmQ6_DZD<;^^qw2+C*B46 zN5n&`+-`^Xi%<9b4LH}~6-Nyx3OZzbMKbjqD!hU=X{cG-cWkkga41xs<%XknuHdGg z8?m(;K1qv!z%mH7Nvm^%sZ(^LcOrI!prqYQ6ar1(Vto6G9$)^b_QWuem~iOnoZ9?> zeGgRPa*`CD8x$b5@S=u%sX;WEuJkREZ4PE05}vfp1--#y1~MM-_chcxu?XoK5Pmq% z75xC@F2=5u+#N$gkXdeTI!@H2@NfdRzrM1{#H};x9xD^K(a@nS^tBi_L8*M*aVDL5 z!p=HZJ>jk-oI*nRu(JaWZhFebW(ge9LXU7qM%5F}i@|(CY4B%uQ%|^1DpKm7ca4jr zKv~?ubSf)+cficLA_mqvGAd{Uw&MYv{mk(B0{hn(oV{0qgTHsSs?;QocfOmChc zz?!fOyHrU6Yf+TEh{Q|_#r8DvY|&6AYqsk#oN7$4O9;(5xI#K07RSMa9C(}S)vqm@nc5nt3v5L7Nfylp2Z1BWc@PjPN+7{iKI=H+h|_{ygScm@ z*Wv&G#z1~XB2^5{mg?)g*zP(i@EwvmUSG4QQeJoRi1X~;>_E!CTd-u2d5GmowX#6C zPHUtD`?%JqR(ga&GjF>^5zvR)c*`PO5wyzZFZslXGr-mHHJ|tz5v60Rv%eNZYuUU; zv@@}o$t&Ny7SzxvkvHJ&4J2;kh0y)U`)ca!L6@opBdi~9=T*_fAOrj+!<@8Px4FH# zR0PMlB@seP1J^t!V!0$R&;SBTb(|2GnbKtJcSinMBygDH4owVTYFck@l^CYK$;3b_ z9{PTrWvj zZ%F6}O2`3whN;h;;xGPD9Q+L$RSwV&5lFs(oVX50PjMZNazstp|CdI5lVnhN!~F=X zys_W0C94#UWt<{HlSFBtV#$4B!=}6tH^g}+Bpop_CrL!1Naw49-!_w)* z_W`j%=`3d`9pWM8D;M;E<%1FwNfohtp!8%&w?QOS!k{2oQH|#vaN+ec9>tk;05~4F zvM!*4!~<8>`&tNm4{pj(;{z0>>7x>sCEF@Ec9U!sRBf(MTYocf3yK;-J|IK~v4mj7 zKp%!o6917*!f`7~y#H@buQ&{fCudKB;A3F&Gz`RHonB$GP-<2tv5(f5Nl>8l@jxt- zp!>8aliL29LMh$Fp)4M`qO@Culw2 z=v8ONv{w~R4bEZl#DI=c8&3>SC+-CQ7939d-uKWi93Xq?7xcLVr+5B?pL-!1YLHb< zqzAH_%OnWB#u^l}=F?05G1flHh@)lSp?BOlzLkD)aBV&PLNsroUmS0{nSOC}?I!x| zh@c|+?F3q;Uo`pw?oR(JKL&K>}o%zqABWa-39Z~^1gxD75l96N|w zn(oSLdZ?Li^P+UqDa4N#hjOXH%E{DiaksG^)i56splqcE45o>T(|u1PI_a{(tu}|b zo%@ACLk(P^g$@45hj>+pmu(^c;Kwu%)Iypq4Nw&tCmc1HLqlum8G}K`ix7VKco7U9 z9WR1fRI?!2A)$wn;??pVHnc!>_frQ}5>^{wG;tQ3A8Jl>aZ{YB&}^P|NfBnit8h(; zaU1P}p)$%bYI5%aM^xbU3~z_uJJtMLx?O?bm~X_LGSq905>q>Y7#A>LJ5vLzCfg^t6BphN`voaf>AwwR% zq%&m5yO(r^jENZ~=?oc@-YVi}$S?^05i%IPMbw|31I_vPC2!PR{XWwq*E0zQo_Pq> zfzx6rou?{F4@dz@=P8fU6H^<_LRB~}llWZ|9dd?OQvgH6WHF^i#=l$UZj1{>CXfKe zzz}`mYCSbDGtow1z%ec}&7sEa$ifg22=P&26OFn>#OfUY)$(CDd+FqN9M(*UJ_iC~ zykShkRSK*a!v(CMh1-YnF<8i;z+kF)mL@^e9X!Q@PCY>`42ll}`JtFpAS6=aVkO=! z)f>2;16@jC8ji(DO~fn9Qyo$j+P7iY$a}Yxyh(9JgfXJKhC_`iAd)=%?kCJ|S@#m< zW5LO^h|^l>JJK4BOcngt*r|a3dx0Vlno5|eP|fUQSxMxLlenok4m4R$ozxctR10%B zowTa%YYE=ATPfUU?$T_8%F7QbSM>5mAh8H^Lc**}2a(h!c_h@_!WR1Zg5|QQi6SuHT!nel?N{+h=oA@OkmiXF&_vCd3{hJ zNu9k7$yq6^=h<#&K6@SirRz_|Z(8YPQ0&a>6bzA3d~X#e}b|vTx)dH$j5QYuS>DpE{QFoGL(PLnTpZ_(?*C>RTnr9*^Xa3(O1&rGi&*iYADw*i+x=y%;;O znKCNZ(jfkg22ob;wtE+j!!08;B8jU+khhu$z+3U0{MT;WL$z;>9QwJPpSk@xq|&Wz zbjX~%`g6N)+X^b}hQbr*H#|{Res1^l4~3W8KetnVxu4*R0CUtyesK+29HalKyOXKA z9-a1XSZjl?^pVNMADiQweIeyR)Qg0_25K}knm)6Yx24;>|W6= z&ojJ(8D4zm%Sk8$^`L*5x955J#25C6-uu>YzFef@BZ)WGR{5qEZ{2HwN@UkB?ISwr zP6Nq)zZr*e>1m=gI+ITBR<-Dp_;kG(7?kklKP+p$wEu4IPP)wNDEUQ$-K)`!UhY@+ zsFGboeymsdtKaEua2*?^hSIx)lpNbj4B)ksk*BO&Yr^=A^6l+bfw@%5Z?;>7RwWWP zj&?f8{!dwLtjFX_i8ph#l#$;#+3M3nNuSo~;(O=4B;W3@i<@KR1Gnn@=6aoDI?9b* zs&k~w7#{#;9}0g|;TN78D7=OX=g7A{z=6PZ6&vNdA6O%;u}?5`+48JUEw}IdNmLcN z<{@h9=_R~?aP7S|1LNsAE~9jZ{vgMGXdP>=nnpe9YQ8v0e)OSrM{4y07!YsyGkMUFaRH>FO^<9VWV8U z)1rG^U*2h*&^|$FVGXc6_yNeLl_6;n%(_E?+1K1KSzffu8s2FMi*`~jdGb&cjr;5# z+hyez`!M0UrSL;EM36Nqtvx+H<|oN-c3DTaS`~VE-e(Y#evP=m292!#td(NleP%^^ zsr=bA+IuG?5J_0fKqBU*_ee;kkCNrzi%xRF=hhNAY`4`pTT=^;3J;}o@#={i<@LL* zVXcB0hmU(KKiX||vKmnEr`8Id^|{scn#|CMO|!pw{F#kk%-I@B??V8y^P>cYlWUCl za^8EdRc{-+@}r&tZ`ns_6zHnHuaTt z;E!Bf58m*Fb)KdR0m(#9V+8bO)XV4b3K79~c+g=`1d zfuIL}xZBF#d})>F5`Lw0374V@$VXKAX@fP{Z9-0@GQ=4Wx$gVQIxzv{?yszfO%TXw zU)kN#{QG>X>SXO#xaoLY&4*vv57=gTm0X-BM#$QK*k_xS`sMWR>Zw7chyPqjqmLX(l<}RCq66E zma%`?-Q_v|v=^K8&(?hVPy3ffcKx$d@_nU?@1rPsL|QuQ!P~ z^3u)WV0%{uOn8<2%?`gQjUVjn^Q!qZB`IsU4zBRF@W-s=kEX7uVj4A)?|*bdI4`+a zL2~^Mc3)$B&HNwi-bP+BtkC9v>jFIt@%A6>Ueu|SpK#-Eql{^k5#o+B4%^Qe<*)bK zzraPl8~5AqL$h7_vwZ@7KEY4N@e`@5K*c^N=r(Q__ZAu9@p9M!`{E(>j0Suov&sm^ zU+ZB2`LASus=^2lx&c>Sd*R=O>t+1`T;cvka{2hHD*uKIAGA9bs8kNxtu!JTsBC^4 zx@NP|2p^~!anN39#+FVej1_nr=r?}D&=yWD^cs><_v^g#?+fOwqBn1#w<9Ta52p_D zs_qZwRbxRZnojzF3J9B&q>m8BwM30mvJ}S86yo@rS4`2iF|lUyuO_z9!_PFO_ovo9=q$F_0DeRfbi2=>x#(#!TuQkvifIegssH8;)E&THq@ zo|@GBVN{L1D($?@T*@DauH2aXqMaCO?!I3#`mb#-+B8OQMCIe}FLn@_jlV||-(@sM zoDxeKZ+vXjr)E-i5QTVono`!2xdFZUKTMUM=ZN*h<4Ze=6V2Yq!axbG0Ev1(a@&O@ z&N4fT^5zBIvd-duR4E7KiEQy_VorJaJ?3b0YgG+I^j^c9A z=xV+`UcTju&au}C4&Z4w=~`Z)S49#$&g4Fe?xqUksXjYzOG@=we{)i*FD7b}Qhkw9 zWh|%}9})S6*|4ytGAiCRz#C(_iy`=VqPsY@bk8Dc*rzt~z?Ls=lyWANU-~d}u`DbS zkDJ@?ll4X7NOSTMDSC*>CEM>~yM_Y%yhT)<7x6Z6B|f^g(i>9uEtXIB5SwYrJAS!S zAeZ(O=f+>c#UeGQ`Khn8SVYZ(OXTQcF+BA>h0v?y#tu$L`B<^Yp*!MVDHa!-yO+vn zFL9x{cd0abiym@WFL9ZEnP}Fl#G1!?i^^Rk*dO2l=Bv0Z*xDz3GvR&JEH?h}P_ z-#&UFs(Z)fMa#MhttiHo(}@Z|mS(*C%GZQC+g*J=3!8Zd$FSGgqnRf)vHb90Gj}X753{M{+UXHGR2v`|ty09$bmowvrG`fmeklTCv zL$dNnk;4_$9x1v}MeiRej-ZO#9VMNEEm!rg`l%8?4_%$`~yQ9UzE^H7X zd@0p!L;GV1Z!X1EW+HHY!FbtspkSNmHv>Vgu!-J2!p@P)2a2*Gtrw(f+TyPFc5ZuJ zX$Lpk&A~DsDm{5nGnH-_B&McrSw(b(EN2ZC=aVhAafod{QF>0;_T+3m7~5E zx$@3?%=U87F`_hg!*eJI!4IF4{|H54!zy|AF(TLat9;-Xah~z9{Nfn#3v!m5s!kEz&FH3wnX1WG;upXps9JkDX5KHqK25~3lQ|s+ z9ea=pV^0@5wLs`1C!7(N4*7@Fjw5G^f@aYuN1rMBS#NCEC}&Ito$a{J$3L*3=1 z@TYRe4REnMf3_$w_e_-Sj&O?22L8>Km4lsbDKn`I$ksU8VP^Pfki*Py>vhfmd2p1N zVeVfdXO0$K%(s`x>e1p!W30UP9C0$ps^+nC#BoOZu~_v$$Rz2#j%(mnzOgk2&xN{Y zHmsE4^Tlx|G^|7uWhJ4I_d@Ip70=2^h>RpB0&J>wpkmBFlD_pKQP6YaBpMc`8dr=6otuvN;jT_TnlTjeL0Kw4w+ zD!dejuD>)%4!;xvqk4(F{8CY5KEc1anYrnnKZn*Wns*sQ$xD}t+@h^CsI=^v;db

^WgTKMy1>WPLX485;x0VTqDl%m3Ny03?`Eh*`0^GyhV&6eeK;_#j*IwxlIfjoCHlXLzadPS0y2iP*6KVR@?@q z?;ZKVZQ>*%{*>P;;_v-iF|12D^E{2qjx2w7WhBF+gTOc} znNHzs%CZ@nWhqmfWo4(|iBpWf$SZy)BF0;C-0v{l@5q<&L$I{DLv-o#5;6Zkvpaxr zacvKUcG$F5U5pGUZ+tiIN} zXo*~Wr#PDQ=B;;%W6VdF;3{GW@O4Y%v3H5<&>Ok>E^$)3aWo1v`Lnhif|ugJUE-PM zC(12<5O?_Q*ADWcyG37X!Sfr*pRT63KFgmcxq#>{J##4M_@kWK2C10%YUza(vSuf~ zTkeU^F`Z@2J)&dsM6-Pv75-Re-mCPZvA4?9r%xr} zzH(~Kl$e-p7?WhdSn)zm?L@%yPULupw! zT*LP2v5lB~3g1qZ-;5VMb0<$k8;KmW(GpAJ;Tf`Tg^1>T$z3pYd0frLUy=O*Sylm= zTs2Wnst`R*SO(P<@NdkQuU06HZy$=2PFgTQn9&zGS)xW55-pstm8!)5HpdQSe9jJ~?}W=vlm*tFYo#xFIOJ^HiBM>|51UE|ecl z5JwS4J57XP0t}CsC`#i)BflMM=Jn(=CM~RH(llpzUnN1uM+YjD)J}wezfZo4pF(bm z&Z1YodO|wQ{e<*?O_l8@sm6|-)VMJ@eUj*xo7@EWNn^d{FOx)f!?>?z-(*-lq$ZS1 z6DP2`FikY{ZJ2`Lj9&nb$Qf`>nzuhG6~|G!Hg;QVpCYGChblY1X5(~W*g5k_+QjFU zH1fJuO_J}<6vvyjlVt8J{BE2ie=!Sy!!oLv1s4;nuaeo~!T}HHq2Ntnu+(VfJ{3)4 zFz5@k5ChQ%wCL&@y{y#mJh^_h=$1h8hqJ{|#*CWmIRJ--;`F(Y2_WQ&b8(0q-d~st zqF+5lemYm2V?I7j9`k@G&P^uW4um2^qJ)>oJ0E~0j$DsYu4M9M0;OC_<<}30JR;Au zd146VK4~88QIP2^^FXGRa?(6SrnU1FnQos45Mg9WOvlJwEX82sA$g^Q=mpIzlfhy9 zs|2oyqUI~gA3k5?6fPx9;I=`Z9dV`5iR>&m}imt{= z`TT?80s^<=0?{`yCTA=V{W`4TQfYKvX1vt>3q&`R`r`tu{`nG2B6pt5S%?TdR5WNI zw8nMQq@gq1u|bc4!=AU*+)N6Sj|X(Vzfg3-s>AmSMgM{q zSp)VZ6df9!C?>r{VtnCi%#MBzrnV8d*pcjG(9B1;Pl$lB7lW2oPpLU}u@FX~TIbYq z3z2}RlWs^SN`+{KX^XMwk$(%GzzPbwI37-FamFN+-fM-sVomT^;Q>MvbtHqf7W-w{9n(XOJ-M&nRPEV7_-Bn^icCx7EFY9FQHDXNPMwRm=_OwqJ!v)a1Cz58bRJm~? z_j%VE(LJfJd9q|Jm;@-hV68a6cn*^rBr#@Af0ub>67Nx@G@$#-T9`%a@0Xbmi>r(k z@|K6iSw@w7;bGCAQ2On|;@m_tXRH(R@$u7j;vYGFq?SQGt8FuAirqXzHasE@5)5BG zDy9>`{NXXo77xe;kBM3IT3XGoXH<);G0@E_3KELK)#d{g^6Qu=mhU_+>Z#!BKZ=u4 z;+sFh1%m3kJRt@qz`EoKm`JnbgHMQ}MB*z?K;uQ$to7pZ?g|@(PJbnzT`rk?rUK?Q z5*{_P){At*+%iQjc@lE*rAak!Jt@|h*>h(H=M0D3cC+R5T5*Y4F}voATJfn7r<+Za zO2 zt(BXf7jC@b_0!}(o)>S$-*--``O^#H0?YN6CwKD1=35qt)Ee(4tcwiYHV$>eh}qgg zHtL~YxV(H5oh=0t#SY@|8o6W>xmG93e{B-SoG_QC+2wkr5o?wq-*B*$3hV6+PO(oK zh8ZJ)q$N{gAGkKz{o8}xL=f47J_=&8hG)!1;pX?ON%vV8? z_sLsc6~9gl>xZvG0<4omHj6R2$*cjPX?g}|dT!0@o5fbcsE|uvhuX7gntbPVab4#P ztBFRnIE)9%?3wkF{I;8wT~qcaalkNl&yct56J6xwH^f>R8F;4h<=Jn-%c@`IJ?M05 ztt^`gv$YdA+je;DeC&qDn#U!%tPn(_CkeQ6p!*CHjPS&=>ed5Cwwk zWWu%bPj88$wjBS2>!?Y=!1J6@`@6`(dQqCWVIx;Pj41Fad0{tN|cpD zvj7LYJvf63$`k>_iS)JNb-D0usNksNPj8D;JLpCsfwKK}WzRp0XfN#zyoFSzf#!|) zS+DwW9Ujrv75Euk=J`FC^k;FFzc?T7f&BE(V)zw_CWh%8e&2jY5COKA!ppL3%Qg^Z z;rWDL#W$t;Bu*0{RxM2YfXpfAwalw}SWfthIJ=;ha^m>K3R*!P8R7Yok8J=na?f92 z(CvLq_S^~+w)_Qo!&Y%|QPZeBb2vf5p?Xoe$eMxzzt{@5Hv;|A-w~brsR+O{=&q@w zqYg&^0%{z9fP(@c`bh!D-UURm(FTOd*X6nIh@(1lJJeMY>Zwue`1jgGOmF%Nvhp3Q zfK|UB`~Njg3)iG+bP?Du{8e~<90^9~JS1hl3;kv61bNiEVpzXKRw5m;!5Js`cONig zgvU~{D@sFG@Bt9gdsp=HMvH3*x93H1iVZAWCpE?#wFA8CpDN-ym6n;h}xd;)K z3<0DHEt-eYZ3O;Jl&}0v^y#~kP(VSch6_gmNgjj3MIl@omuj0HenxivyST}$Tv#*X z@8UEA5AyHZv8ILJF7Jy0^gH@}h|cl{<)rt;ezS6Z&4dpijOpR?55@WPAcub>R#E`* zx3%Ww@}!U9t7?!HAB*Bs8W~w(LnuLDywNC3lhf z3x8^ccDASYq!FIGLdJH9vyB(!Tf0P=xpjpcmTPsFmwzUHJ~l$h8lkf)X^@dhD%4?k^LBG>K~eQa9nkXPPs zM$GTW$q&E8y4(So^SM}LzEB~D33Cl4U-kvop%2JqGtDk?{uknkp$rjJg50w38e#y_ zb^QidoAld2n?AcBJA!NPlSh6jt~FPzR+Du7sb=>;!~4Dz%c#&P>V^hlUmqu;iOuxjM~U+bHC-Co#_8lI!-kD5^b+1G4}2qz^-t&V zquvyxoP%(WD(gA-E-gk!U>&DCJjmo6fY5J~H2`iLu8v95Nz-daROe`zQasjzee`?PV|avP}|LKc~q7d(Cs@O+9T#cH1wGuqM5#IR%<%3j^6~wU)tl9?^%SzgJWw(?Wo0nygRiJbWG1q3Ks(o69<1B^{$=2|!2I%M zx$I}u`oYWc1^V5p-(S`5&*B~PQOeqv6QEf0Uk)}sl|QL@!rZ$_&N_g4c5mWx=KGtd z8S9-*!Cr2`TO2NPBWE$6;-8eIo2a2qs?T$DEtAx*_5DkMhOMvg#-aSwy_64MqJkQX zzhHg>*d`hxq*;$sL4P_mSG~l2-}EB=7!S(lf5v+2{hVk`LC*Nfy1D;FnPNESnbl8F zpni`0@H(8(f2ZNJ&#C=0HAl@Q0qUAyjTQkXROt~sIW$?yo;Va za)UJJRJlL|YX4dUYTt9TW2Zo5g~)gN_{~m9%}56ugTuA;sDu_|fN0CBeY0j}igUAJ zLUb8zoC zYfGU(PCQ@?m(hG@%5V*We=sGh@^(Zpb43UWC=dns(Pm>VCOu+p9e}jrS@~tYGdSD- zn)p(&K@KW#ju%|NTzoY6a9)9PsyX?2`B8zBN6wO;3!Gp1IH!#QHxxQ?0~vAyE)4l} zp)=E}U%F8qn`?EGH+OM*_t{6;dFz8Ih?d5>_lsKve`kt z+r>GKq7*xI#c7dfc1TxrYtvJ5>~A0_ZtLot+*RWM27Q6{X`miDx>HvZ*wFR^way#b zs9|ZiPQKsy!7jAPf#y;8S+rU9cb#1G=W)u_G+vH#os($s>~+^U+H9zkxC1OVua=kw zeg-xYGeE$=HB3ftoBD3l$}=KP-|l+XBsxd%Bs8e-9#OXrYC>tV9;eS=v%yIF${t$Ia4zI31MV&}C?XX7QMk%fjyn&=Fi$RLUPbFVhaTov-|PrUQTz^esM3S$lP2f$M$kM z(C@rni1j*1LkAPSu9p+FrtD1Yj!T)>+sTRfQzo>mosd76FOi)X#|`y0;QzcmYI&9P zg=8hi@_}l$?usYS_FsJ%KUfO0M*Fdk{7=zF=2l>iF8nH)rsh7T|m*89+3Wdt=HX|h!$E3)?3=&iO9SAIa$pI`fWHw*?vumRUhkgcP6#c;<}@ohqGJC z+}4T#&~8~p9vy(G)S+er6Zakh-iZA0Y@8dU$`;DI2czRjk&T;LgP~>6x3*S{K$qb` z2sUaK<`su1+ZxQ}zW}xm+2_9vX=Z$RpeDC6P)r)l7UdYwy@vq1C<6yZv^G}j4^csD z{bmSuwpPJ}!j#Op@=N$uQMEkqb_Ss-TwP&eT{xBX{n}&GO;A zQOcoq93s7N3Wc?IO6$aCK*(FNir>?5D-qN3qE(yZ z#t*HIX*hI}6P8Yq-(KWoXJQQx7Y}jLBZ!0<4SIikihTQg)PB7I%Si*^?LFUQFQ&*sj6Sh#JkQ3Fy^u6f{E*=5~!Nt ztAlyhoSVoy?>y*m^U=#K=rUJZ7tDJ7d5NsD?mSdGmod^@wI$~#@@80MvvCjBOqDav zNB0r7!Kg$;AE&>~=uP(xf` z(b%_J344lD6=h^g?-x!lbJ_ycG9#4fxf#WZoELJ`#m?T?j#*6cMDTOzNT9^)^yJry zwc`E`5Zl`q96!4xPA_98A%P64AQCqN!0CxF+J6Tt4t2@p)l z3E5>`8>M9642=W|>SoC+FL6G|-9t$b0aY{Um##?!j2$2XF1*w^*IYg$K*CjY>IN{5 z!7h|3mpOxJ#J$Tfxh|X$m<4?XYLO=7ahF3NXaXT}=wr^CLHu1{&PIF0UC&?c06NKvnEZWkqMC$m4(Q{MX%Pqr{D1&Y(&sKTKYDD8tfc#2)jYv z(%)`xzQ2eyt7Z(CzBfA_N%&iDR=Yl6Rdke%*9k`Z)|Tvii}NLnEpaQX31w^zKO8G+ z$*s;4dd2S!{i`;D_6Vs+yP&VWY0?++=IIy^6w9gKP@JGq9gAY?Nh<7AlmK9hJ^Bgc?ky*=&hQvv`D8n{SK0H_mY5m zSWfcImsL&dHiiHCB}&rmAZ?24*Rt)f&|e~N`MuNqlw?~bV7-(;5F`u~5P0AX1Qd7! z(P2mZ_TM`vnme(>evETv?h3Mky!PHYs)3K;0(GZ=x)+XdZZ-NY_s7JY*NXE8?pj5KosuF53%PY%F8hqGlhm+hoLwq=}+h8?vjNPXOn)D5pmf=mvK z!LpkDxI8`Pl=gpyzY)&SXU4gzY390MpGWALW>(4HpwIG!7|ac&RCJUd#+=Ne$&771 zc9oA(VIPd+bzvwL?aG~QMf;gh_4p-cJyvI}*6rLsVB@U`;&l(fZTcHS)@_PHERo zj1E#LXg(%D6*^Wt!sw8XjCF<%PjDL8Ym3eV_S&u=AqX1uV{$DDsb9(BaZaS`ddlL_ zq>D@|bbgGX-_&*EoRXoFITPUumHO~BmtQe;D5Jmb&~t0#TjS8zzjDVqrS;Y6pRbYa z#yj1*zqE>4r->0wCbMLT&ib9aak7&uZyE0l9zB+rmRSQ`-b}2ClIZd_{RrrG>qkJh zPd~W4Eh-Z zO{0UFYm?+ikW`246 z1Sg|t5+jH74uG|YnvB!FtMfm%mPLIG%HXNl2w$)(u0LX7M)`KjV?=2b42E99h?B&J;VI0`ElnM$SR%eOiS!-~g>$?8lWZ zVinB_hic_wr-TclRR~QzFZ_=5q_}-oh&7?rs#wUGiab(54j*|H=S7Fj8P$FkX4KiW zraQz(14ZiXwdBl$ErmH`%guxHL^r5F7{f=z7;TUa>EtXe(D9Ir#P0uRLgqZDX9ML( z9v3;t#?T*OW91`64<1UfLLp|FPsO>712`$>Z+8kgb0keU{?2SIFnF0rp#`5NJ6X0- zr!?lh5Nh;5SiIT(r-jV+yYYjwHUHp?LfF)Zi?A_xS9tq5sP=>qqUlkwS}F8839T1G zwB|os$T>%gw5(WZI6!f1w_&AuQb#9y`QmifIMlJo13e+OWF?>Qj@H}UzW2qC5x#S= zFUOA&H|J{!W^@lGex%W#nb?vKUVx*bujtlKNK9H_WT!`poE_fNwzyfYX7k@a(B{{i zmm_}Bi9j!K&q5f;!W_QhLv3ochuL(E@sn|@X{8H_rSmKNM&i<}mVEX{S^%2-#Yayy zxg}5kSfsu8@iWrUV#+7mTA|F&T%?WQPx`^X;o7?L-^THHk_Vr&5Wju>_h*#C7GXDU zTpB&~(ACXFcs;lgBR626X-h z-J>8L2*U-fK7%;#2*uJtoLe*(k4dZcXTALhHx%I68}V=+ztJA21IBND@m!oUiFskLg7M=#qc&kn<_7=h_*Mm$xX^;6u7fE#zPgtut+ z~og!qgV+LmWtKL4k;j6VKMtK(lZlg7zQu`2q{B6urC^`5`%K+QgM_s@wUR7y7{ z>9;7+ex?-sYq(SR2L;de+40PvMcgH{km7B)7s{@5bi`UdClvz7T3C%Rbpun!Wl&yW$3hulho3`rK;jSB&LHsu2Cb(>o<*5G4tKGO~i8rMf}5+xC3lo5kI+7 zo9$!fq9eYDQ9sg+}{G5x>3)Kgb}>ceNH@_lVgd^mHEu@dZ!MQ+RkAt&irF_wP+@N}kf3qGxtlZ-NTCYVm5z`WH(v=g`>S=#W zujudTqvC~mqSg>7UBAU+a0GUrL(F#!MhFVB;cOVmyojeQNjJIFq(54ap#9&JRjFJ+>FR9=7rTwj2+rhvI#$Uh`DEY7Wn1z znb_EviKZ~-ws)S^*K)Cl56#o+SKlFg##F1P>4#?JY5CzdN-zYNkZ@RZI}#=)c~%9V zi4m0-n0bWnf)|aZ>v8qeH@GkX-$)W;3v=*=(cvJ%> z)@+B1iOHpI&<-u1>doJ+1y+}~lnY{}?tDr|EyPnab z-*(~X9NllV8;$?7o?Y0jP0`JEp^Y@BP;Od*7A;58^2C&ABykfvIz=O?7$b>!XpH3H z0&Sz8Xq-i5zTZXR-lms&dB36XMf(kf z2Nb!Ol5|U5i?o93t_@f5lFoX?T77gMShldO z)bEp!%~-;OeM7)*7$lyTq(uBOV^yg{5`ScriH!`T>;+IJS~cj1)~<)x^w}M_a7F_% z@u0r>RUdC;)PJRH;^QIJ@QE#AjQ_psqQ%tN8PqVl0~1I8jsJ8+YvzBQq-bYpSey!< zorlfhQLVn?JS7<~2Eymi^oxSUcbL$=3l#RY7b^N~3K>s{=+`JruNN={pb%4VZ@C~$ z14AX+K|L7PP&|L@y+~Ev9r|4=fSO`&tN+2b9EF9?*vHQu#YO(Kx48E)t$m=>mCbyl z)O>Lfv_@Y|7;{X!tEms2!%k_@?&|N(`De9!b;KcBgl*Kd%@h*fh7uv{dME-LM&zfr z>Gce-tC{vQg@~m0$Jz_?DMV+7HTAdXQCII_cxIUVB(y!9B5VpCcg;D62frTkgXeGz z^VoUioOVrBXYwC@*0OvS?Q_Qtg05=4AMAAwzM%E+RMzvIw zsxlm{D0O5wN>LihaHOI{$Z!iqiI?FB#i4XFndnw+BB{cG;GfkXq>Bb+n(5JK&wCkbr%;c%0d3tU2Ua0mN)-R3-CFl>q=FKdwD^J{Ken2 zW~$1kBSo#xm;R>pQ77?hAR(d{$d&l&iW|uT@mZ;_rKU|vZHMrAKJ*@LB)x}Z9qh#P8h0UIlOTy{~1cTlwt!+9WlMs}HpKK8I@P${@(B0sQd@ zCF>dVVq!vSYFef>Au%y2Bg2|t9XK{UDPfegS6X61igiF{T6)5mBR@}5tj^1{(* z#4QGv084>|z(0Xcfn~sQULAqR_B)=;w)!YEDw5w$H#JG zdGAMBD0k0ct@w!FSpA0M6DB;Ll$x171%r1ZR#!$KR~E+33Q8Zwqd&gsk(M1etCg+{ zM(o>gFjYV${|@nddKmNPA3nylvy4B29#Z2aKL`z~<-#v8`D|ZbWjNw{p&TF3e0QUB zd7z}E#h}!epc)3P4+>G`Q^ZsGIcE9>P&WmR=5qw7fqOuyfMSy#0Y%T0z8*$F3&A6U z@)0Oi&;;?M_2s`h8(BaK}#=?tiv_?zG<|1MDK@f|b09xA5kdCHVq@9(RGl8SzZ z0JYG=Y*|&ar8+1TSj*$%BGxhTGgE_$L8-mFKuJNHK`DE#IS=}p^Q4ClZ>O>#%e^*y zn99aE&Y33B4wOb96_oN#2Bm4R1M`Ypx$MU;h^K)l1Erbo0mV}L=YXeX&j+P}SPn|< zS!2q5h;m3SA3T+>BOl}(iX5R?5TFD(Qhx_e4TvxVm3C%D-9ah-MKeATlq?_v)DLtf zs6Xf@X8c-EtXxU~DkjB9M-h$ktCOjDniYKm!MYGQW@fkqN)76SjMU@X;HhWRF+wx| zDkzPp2PpMS2PKmY2Bn^lF)Pf5sZn|(#FK{lB3^V1ybqb$+X#?Jv<0PtE8FN)vw%5B zr=FyOrydUlr6K4ENlD_ zW^f*mBLz>|!ymY_dS2VXlN;RGo(FlbT3#*p8cX51_Pm`33w=(OQxz_cnj`lg6)2zh z2pbh+fu`M!XTay83Ztk=dkkuAJYY&{ViO+Igawfbmv_*WI-n;NUp~AE3vrYnA^;Ix zQfc3inlL^oJUkqHg}T583>KTa@d1M)ddf5%TU8p^+MqOtg-m3dHZnchOqcsrNu_*5 zc$8Occ%-6?f}qAXcrebFHDrw)U!X8DRT_hpfGfYsjEm@~E2|M+1FQws0ogzfKvqh5 zzpTWsH{()}Hy7az0Oh4H<=qI>B;;B4}yMZR`2Q30f zZ^Z!g7FVIN`iQJtImLx_)|C>_13)Qo5Xd%mF4dZ{aKD*I4?$cnfTqmM&d%OV*_*0* zgMZhIJrAOWv1TC6!&q04ED*j^&CB|Ql|^4rP_n23mmE&_iAFDi5nZO;05^t;$gS7=hc0SHHC@?`r|kxVDG zy7Dw9TzRP5;Q6CS*8Vw}ji!aGm=~%iKPvwCD^V;sM2glzQb)u~z5{qt)kVH2iba(- z^hZRyme>I9j!Kl-hJY8@%#LgW-_<4kd+-t7y3zpbHvUE!tIN})nO~@k zqv=CcMLi;8q!BTWw~ z>4b#THIfmQ__ z3QCLcpQX#oB-yODPa10nRYCb_Rvrev2afQ5F{}Z;oskv8dg48cJ7ZWQOME|mIfk|K zPw8jaP&}v=!V6KW4-aX}YWj>GpZR=h)_A%G%<`V&yw&*W_q9bisZSEe& z>R3h%bOyz-NY!KEi@MU3+Fyr{Z^OL%mw=~f*b(MJs&nP3KFJS4Iyqy>w?sc_)=56G zoaaStSa@i!LAnx%bh^R{1tdQbVba5p$%-;}5D$un?!JMnH{$kzQsZ#}J2jp)t#=Q6 zHSpR?x)@R~P~Ax170=>qGG9F6sg9ALJR)tC)P+k$?tSxQR=^Vlg=!FPwvcW@|Qa^ zYsgQ?<7(BXEAeOeqRuQQl})Q zjY@jG`5O^3Er^ukBHIEy4TemQ2H(Vm?+2a?AUQRYAHTq!8=(@P1zk{%7D9_1ewOhzymLi%g zM)I^jv$^oJFC%>(=XbK%@RpOY$?gl51``upbPGgu>zv8cFcQuKKLZ!|>>M`6Pupi4 zpCllPG_URBmM>XX$6WG320ec(w{LaT#&g*cBmY$rD znyDzA_Zto&GiyQ$o${H{$`HhpU8g2Z%A7KxJf*#oiG=D%$VyGll=5>CN1iST-hCOu z)Hj?ODcK0qqLYy{F)JxGF=_LD-g`Z(HEti`$qV(FKqrSN1!s$A%reY!5rd0x#E~hD zoPtxig!J@;DcCOJJdfhg5jyTu>Vs$C6UQc`CnVCv#fTAMh$HU@Z&ix=9NoYhUQV6` zW+sgxNyv;v8clGy0FF#cOG!#d6=^dnrKP1NS<^;am1T&hI>W5k(<6)-Ri0m#GlEV@ zM~HYoG594AG+Zbe!TW7so%rTl))4FGsazJ|aS>%);*WCK0Q?Sw)WIY773<2&h+Pg# zqM?<1A@~|D{88``E_^X~aw}5)Aoyq(J`~M0@;j?-WYzE)(qd=OCRR_aas^UXfot5J z$2c8hpUh*!BBhNzu4H4xhd^AWd!c)V4f1hcv37n6Y_%uS|A0M^Ee$#7%>RlVQyqK2 z(5gaq@@G47cbw~%ApHPP3LFHo0ooU(Bx4|~iD@ZW<5R7OlbeIM|r)itexfSqqud0 zh2ufX*;`qZU(I8>@XPN)#1Ci~z)QE{#*%@@oZ2?#=cYd5b+@xdEgmE74}iP~>4EIm z+Bhx4nnXirZ6Zhb$PdQMKFud0n|hBg+RmEU8e_9co;$;|Y{{PiPX^ieB)_noh5BVe zjx1kpZV?}}gT?r5Mm()1129l^KsTS{qj#{TA!Uds`y|Inb6B=cMc4wk@eMmzjqcI^ zGF&1pK(1o$5b*#sz%`yWO`bp%KvwO5KMCU>rUornfEW=bSG=-*oLyH`V;61 zHBIu5z&CK={{T;`kWBY>Gi+4ywJY&U!P{Kq-v=M+!gGV~U0&dHL|9z}-mFx>rb_&` zMgisd_Ylt$c47Ox@19mabn}mfZr?|lG}^A#P6twSal83P{O5s#d^uPRdR;E zRmdVO`dOY|$b!^5{D(popw@NXDP&O=kN3~%N<^m7cq@@-_L?AdFS}J{Vd$$ zArh@FiCZYKA>K&1pEb3loab5*vsvby=S_=PJ&(NexBx2`_OTo~&r=bPdq2^p^ZY%8 z)d0Sxh{gJJ{Mopc8iD4TzaUwP*{hbkpZTz27UO#r=``x(|HuVj^9R3El13z{K zI0|eBHUg`FkAR8rN&iQ1h=NC;g5E$oARGt)ssIm9=*o5A2jBp(3&;T$0<(eFfw4eG zzzevEj+_Kafjz)hU>)!g@H#La7yNM<5)i19$_+uR>+OA|MSI40O5*)x{#v81Mydp}@01G4L($B|w$T1EvFMK#wcV zaX+%_Rcc881a~Ri7P-yWF0c@_jK6zoC#eUON zU*#pFM8SEs0iCAuzg}c-@vuuw=MT5wzxYe6FJE$rS$Xy)oJAZ$sG9uM`D5hpgMKbC z8_&MXLb&f`w#qlm^tH0)=6wHU_B|Xib>onlkvS!$vNqaq^;h&io(B>M z<*k2Z>xUk!V*LF0p?Uwpaq!fyQ=qg(y$niwfX62F^YRrJ(_x^rR@*^oL-jH!$+Zt8)sJ1RBsoDlg+lMevs;V_8U1oMR`F^0Ji9w*$q#0^rs{Dzs-8Km3NfP!Y}4Dz)L{MP)>r1+Ciyz+f3?OU?;xx zHhaBE=SstA2j3}e{P={_Q5E#rh1<(mZS@8IN*NojcI8{juxIba&z7;SE#>$vz&Mh! zmVlD-mV=@=WrayU1EuDzq;!7Z6806{Rlf2rCgZX@EZ5OT_Z1ssS>71%v`0z5`HdC2seO3A z-`QvA_?_QbBmVb0Oo#b4f5@Kq?fWFF$k~sNe+c95&u2fx#2>)dJY=t{f&95g_!Vg& z@ArrW`@UEy%OF1e5ex9`R4Mu;p7V&s`aU%_{LUj5CH#{5GH?ADBhc8H@R&`q)O`gd z7#GnMyZh?LH$5Au4YMO}oZnhJ^EE$w5KQr=OAV!Sps%=->3;yC CB{ju=4wmyrTTXLb$zqGMK948>&x`r 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 }*> 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 z delta 98322 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, + pub cwd: Option, +} + +impl FileToOpen { + pub fn new>(path: P) -> Self { + FileToOpen { + path: path.as_ref().to_path_buf(), + ..Default::default() + } + } + pub fn with_line_number(mut self, line_number: usize) -> Self { + self.line_number = Some(line_number); + self + } + pub fn with_cwd(mut self, cwd: PathBuf) -> Self { + self.cwd = Some(cwd); + self + } +} + +#[derive(Debug, Default, Clone)] +pub struct CommandToRun { + pub path: PathBuf, + pub args: Vec, + pub cwd: Option, +} + +impl CommandToRun { + pub fn new>(path: P) -> Self { + CommandToRun { + path: path.as_ref().to_path_buf(), + ..Default::default() + } + } + pub fn new_with_args, A: AsRef>(path: P, args: Vec) -> Self { + CommandToRun { + path: path.as_ref().to_path_buf(), + args: args.into_iter().map(|a| a.as_ref().to_owned()).collect(), + ..Default::default() + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct PluginMessage { + pub name: String, + pub payload: String, + pub worker_name: Option, +} + +impl PluginMessage { + pub fn new_to_worker(worker_name: &str, message: &str, payload: &str) -> Self { + PluginMessage { + name: message.to_owned(), + payload: payload.to_owned(), + worker_name: Some(worker_name.to_owned()), + } + } + pub fn new_to_plugin(message: &str, payload: &str) -> Self { + PluginMessage { + name: message.to_owned(), + payload: payload.to_owned(), + worker_name: None, + } + } +} + +#[derive(Debug, Clone)] +pub enum PluginCommand { + Subscribe(HashSet), + Unsubscribe(HashSet), + SetSelectable(bool), + GetPluginIds, + GetZellijVersion, + OpenFile(FileToOpen), + OpenFileFloating(FileToOpen), + OpenTerminal(FileToOpen), // only used for the path as cwd + OpenTerminalFloating(FileToOpen), // only used for the path as cwd + OpenCommandPane(CommandToRun), + OpenCommandPaneFloating(CommandToRun), + SwitchTabTo(u32), // tab index + SetTimeout(f32), // seconds + ExecCmd(Vec), + PostMessageTo(PluginMessage), + PostMessageToPlugin(PluginMessage), + HideSelf, + ShowSelf(bool), // bool - should float if hidden + SwitchToMode(InputMode), + NewTabsWithLayout(String), // raw kdl layout + NewTab, + GoToNextTab, + GoToPreviousTab, + Resize(Resize), + ResizeWithDirection(ResizeStrategy), + FocusNextPane, + FocusPreviousPane, + MoveFocus(Direction), + MoveFocusOrTab(Direction), + Detach, + EditScrollback, + Write(Vec), // bytes + WriteChars(String), + ToggleTab, + MovePane, + MovePaneWithDirection(Direction), + ClearScreen, + ScrollUp, + ScrollDown, + ScrollToTop, + ScrollToBottom, + PageScrollUp, + PageScrollDown, + ToggleFocusFullscreen, + TogglePaneFrames, + TogglePaneEmbedOrEject, + UndoRenamePane, + CloseFocus, + ToggleActiveTabSync, + CloseFocusedTab, + UndoRenameTab, + QuitZellij, + PreviousSwapLayout, + NextSwapLayout, + GoToTabName(String), + FocusOrCreateTab(String), + GoToTab(u32), // tab index + StartOrReloadPlugin(String), // plugin url (eg. file:/path/to/plugin.wasm) + CloseTerminalPane(u32), // terminal pane id + ClosePluginPane(u32), // plugin pane id + FocusTerminalPane(u32, bool), // terminal pane id, should_float_if_hidden + FocusPluginPane(u32, bool), // plugin pane id, should_float_if_hidden + RenameTerminalPane(u32, String), // terminal pane id, new name + RenamePluginPane(u32, String), // plugin pane id, new name + RenameTab(u32, String), // tab index, new name + ReportPanic(String), // stringified panic +} diff --git a/zellij-utils/src/lib.rs b/zellij-utils/src/lib.rs index dd314a8992..63bf71c2d0 100644 --- a/zellij-utils/src/lib.rs +++ b/zellij-utils/src/lib.rs @@ -6,7 +6,7 @@ pub mod errors; pub mod input; pub mod kdl; pub mod pane_size; -pub mod persistence; +pub mod plugin_api; pub mod position; pub mod setup; pub mod shared; @@ -25,3 +25,5 @@ pub use ::{ anyhow, async_channel, async_std, clap, interprocess, lazy_static, libc, miette, nix, notify_debouncer_full, regex, serde, signal_hook, tempfile, termwiz, vte, }; + +pub use ::prost; diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto new file mode 100644 index 0000000000..05fa5c6ed0 --- /dev/null +++ b/zellij-utils/src/plugin_api/action.proto @@ -0,0 +1,246 @@ +syntax = "proto3"; + +import "input_mode.proto"; +import "resize.proto"; + +package api.action; + +message Action { + ActionName name = 1; + oneof optional_payload { + SwitchToModePayload switch_to_mode_payload = 2; + WritePayload write_payload = 3; + WriteCharsPayload write_chars_payload = 4; + SwitchToModePayload switch_mode_for_all_clients_payload = 5; + resize.Resize resize_payload = 6; + resize.ResizeDirection move_focus_payload = 7; + resize.ResizeDirection move_focus_or_tab_payload = 8; + MovePanePayload move_pane_payload = 9; + DumpScreenPayload dump_screen_payload = 10; + ScrollAtPayload scroll_up_at_payload = 11; + ScrollAtPayload scroll_down_at_payload = 12; + NewPanePayload new_pane_payload = 13; + EditFilePayload edit_file_payload = 14; + NewFloatingPanePayload new_floating_pane_payload = 15; + NewTiledPanePayload new_tiled_pane_payload = 16; + bytes pane_name_input_payload = 17; + uint32 go_to_tab_payload = 18; + GoToTabNamePayload go_to_tab_name_payload = 19; + bytes tab_name_input_payload = 20; + RunCommandAction run_payload = 21; + Position left_click_payload = 22; + Position right_click_payload = 23; + Position middle_click_payload = 24; + LaunchOrFocusPluginPayload launch_or_focus_plugin_payload = 25; + Position left_mouse_release_payload = 26; + Position right_mouse_release_payload = 27; + Position middle_mouse_release_payload = 28; + Position mouse_hold_left_payload = 29; + Position mouse_hold_right_payload = 30; + Position mouse_hold_middle_payload = 31; + bytes search_input_payload = 32; + SearchDirection search_payload = 33; + SearchOption search_toggle_option_payload = 34; + NewPluginPanePayload new_tiled_plugin_pane_payload = 35; + NewPluginPanePayload new_floating_plugin_pane_payload = 36; + string start_or_reload_plugin_payload = 37; + uint32 close_terminal_pane_payload = 38; + uint32 close_plugin_pane_payload = 39; + PaneIdAndShouldFloat focus_terminal_pane_with_id_payload = 40; + PaneIdAndShouldFloat focus_plugin_pane_with_id_payload = 41; + IdAndName rename_terminal_pane_payload = 42; + IdAndName rename_plugin_pane_payload = 43; + IdAndName rename_tab_payload = 44; + } +} + +message IdAndName { + bytes name = 1; + uint32 id = 2; +} + +message PaneIdAndShouldFloat { + uint32 pane_id = 1; + bool should_float_if_hidden = 2; +} + +message NewPluginPanePayload { + string plugin_url = 1; + optional string pane_name = 2; +} + +enum SearchDirection { + Up = 0; + Down = 1; +} + +enum SearchOption { + CaseSensitivity = 0; + WholeWord = 1; + Wrap = 2; +} + +message LaunchOrFocusPluginPayload { + string plugin_url = 1; + bool should_float = 2; + optional PluginConfiguration plugin_configuration = 3; +} + +message GoToTabNamePayload { + string tab_name = 1; + bool create = 2; +} + +message NewFloatingPanePayload { + optional RunCommandAction command = 1; +} + +message NewTiledPanePayload { + optional RunCommandAction command = 1; + optional resize.ResizeDirection direction = 2; +} + +message MovePanePayload { + optional resize.ResizeDirection direction = 1; +} + +message EditFilePayload { + string file_to_edit = 1; + optional uint32 line_number = 2; + optional string cwd = 3; + optional resize.ResizeDirection direction = 4; + bool should_float = 5; +} + +message ScrollAtPayload { + Position position = 1; +} + +message NewPanePayload { + optional resize.ResizeDirection direction = 1; + optional string pane_name = 2; +} + +message SwitchToModePayload { + input_mode.InputMode input_mode = 1; +} + +message WritePayload { + bytes bytes_to_write = 1; +} + +message WriteCharsPayload { + string chars = 1; +} + +message DumpScreenPayload { + string file_path = 1; + bool include_scrollback = 2; +} + +enum ActionName { + Quit = 0; + Write = 1; + WriteChars = 2; + SwitchToMode = 3; + SwitchModeForAllClients = 4; + Resize = 5; + FocusNextPane = 6; + FocusPreviousPane = 7; + SwitchFocus = 8; + MoveFocus = 9; + MoveFocusOrTab = 10; + MovePane = 11; + MovePaneBackwards = 12; + ClearScreen = 13; + DumpScreen = 14; + EditScrollback = 15; + ScrollUp = 16; + ScrollUpAt = 17; + ScrollDown = 18; + ScrollDownAt = 19; + ScrollToBottom = 20; + ScrollToTop = 21; + PageScrollUp = 22; + PageScrollDown = 23; + HalfPageScrollUp = 24; + HalfPageScrollDown = 25; + ToggleFocusFullscreen = 26; + TogglePaneFrames = 27; + ToggleActiveSyncTab = 28; + NewPane = 29; + EditFile = 30; + NewFloatingPane = 31; + NewTiledPane = 32; + TogglePaneEmbedOrFloating = 33; + ToggleFloatingPanes = 34; + CloseFocus = 35; + PaneNameInput = 36; + UndoRenamePane = 37; + NewTab = 38; + NoOp = 39; + GoToNextTab = 40; + GoToPreviousTab = 41; + CloseTab = 42; + GoToTab = 43; + GoToTabName = 44; + ToggleTab = 45; + TabNameInput = 46; + UndoRenameTab = 47; + Run = 48; + Detach = 49; + LeftClick = 50; + RightClick = 51; + MiddleClick = 52; + LaunchOrFocusPlugin = 53; + LeftMouseRelease = 54; + RightMouseRelease = 55; + MiddleMouseRelease = 56; + MouseHoldLeft = 57; + MouseHoldRight = 58; + MouseHoldMiddle = 59; + SearchInput = 60; + Search = 61; + SearchToggleOption = 62; + ToggleMouseMode = 63; + PreviousSwapLayout = 64; + NextSwapLayout = 65; + QueryTabNames = 66; + NewTiledPluginPane = 67; + NewFloatingPluginPane = 68; + StartOrReloadPlugin = 69; + CloseTerminalPane = 70; + ClosePluginPane = 71; + FocusTerminalPaneWithId = 72; + FocusPluginPaneWithId = 73; + RenameTerminalPane = 74; + RenamePluginPane = 75; + RenameTab = 76; + BreakPane = 77; + BreakPaneRight = 78; + BreakPaneLeft = 79; +} + +message Position { + int64 line = 1; + int64 column = 2; +} + +message RunCommandAction { + string command = 1; + repeated string args = 2; + optional string cwd = 3; + optional resize.ResizeDirection direction = 4; + optional string pane_name = 5; + bool hold_on_close = 6; + bool hold_on_start = 7; +} + +message PluginConfiguration { + repeated NameAndValue name_and_value = 1; +} + +message NameAndValue { + string name = 1; + string value = 2; +} diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs new file mode 100644 index 0000000000..1a964fa9bb --- /dev/null +++ b/zellij-utils/src/plugin_api/action.rs @@ -0,0 +1,1304 @@ +pub use super::generated_api::api::{ + action::{ + action::OptionalPayload, Action as ProtobufAction, ActionName as ProtobufActionName, + DumpScreenPayload, EditFilePayload, GoToTabNamePayload, IdAndName, + LaunchOrFocusPluginPayload, MovePanePayload, NameAndValue as ProtobufNameAndValue, + NewFloatingPanePayload, NewPanePayload, NewPluginPanePayload, NewTiledPanePayload, + PaneIdAndShouldFloat, PluginConfiguration as ProtobufPluginConfiguration, + Position as ProtobufPosition, RunCommandAction as ProtobufRunCommandAction, + ScrollAtPayload, SearchDirection as ProtobufSearchDirection, + SearchOption as ProtobufSearchOption, SwitchToModePayload, WriteCharsPayload, WritePayload, + }, + input_mode::InputMode as ProtobufInputMode, + resize::{Resize as ProtobufResize, ResizeDirection as ProtobufResizeDirection}, +}; +use crate::data::{Direction, InputMode, ResizeStrategy}; +use crate::errors::prelude::*; +use crate::input::actions::Action; +use crate::input::actions::{SearchDirection, SearchOption}; +use crate::input::command::RunCommandAction; +use crate::input::layout::{PluginUserConfiguration, RunPlugin, RunPluginLocation}; +use crate::position::Position; +use url::Url; + +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::path::PathBuf; + +impl TryFrom for Action { + type Error = &'static str; + fn try_from(protobuf_action: ProtobufAction) -> Result { + match ProtobufActionName::from_i32(protobuf_action.name) { + Some(ProtobufActionName::Quit) => match protobuf_action.optional_payload { + Some(_) => Err("The Quit Action should not have a payload"), + None => Ok(Action::Quit), + }, + Some(ProtobufActionName::Write) => match protobuf_action.optional_payload { + Some(OptionalPayload::WritePayload(write_payload)) => { + Ok(Action::Write(write_payload.bytes_to_write)) + }, + _ => Err("Wrong payload for Action::Write"), + }, + Some(ProtobufActionName::WriteChars) => match protobuf_action.optional_payload { + Some(OptionalPayload::WriteCharsPayload(write_chars_payload)) => { + Ok(Action::WriteChars(write_chars_payload.chars)) + }, + _ => Err("Wrong payload for Action::WriteChars"), + }, + Some(ProtobufActionName::SwitchToMode) => match protobuf_action.optional_payload { + Some(OptionalPayload::SwitchToModePayload(switch_to_mode_payload)) => { + let input_mode: InputMode = + ProtobufInputMode::from_i32(switch_to_mode_payload.input_mode) + .ok_or("Malformed input mode for SwitchToMode Action")? + .try_into()?; + Ok(Action::SwitchToMode(input_mode)) + }, + _ => Err("Wrong payload for Action::SwitchToModePayload"), + }, + Some(ProtobufActionName::SwitchModeForAllClients) => { + match protobuf_action.optional_payload { + Some(OptionalPayload::SwitchModeForAllClientsPayload( + switch_to_mode_payload, + )) => { + let input_mode: InputMode = + ProtobufInputMode::from_i32(switch_to_mode_payload.input_mode) + .ok_or("Malformed input mode for SwitchToMode Action")? + .try_into()?; + Ok(Action::SwitchModeForAllClients(input_mode)) + }, + _ => Err("Wrong payload for Action::SwitchModeForAllClients"), + } + }, + Some(ProtobufActionName::Resize) => match protobuf_action.optional_payload { + Some(OptionalPayload::ResizePayload(resize_payload)) => { + let resize_strategy: ResizeStrategy = resize_payload.try_into()?; + Ok(Action::Resize( + resize_strategy.resize, + resize_strategy.direction, + )) + }, + _ => Err("Wrong payload for Action::Resize"), + }, + Some(ProtobufActionName::FocusNextPane) => match protobuf_action.optional_payload { + Some(_) => Err("FocusNextPane should not have a payload"), + None => Ok(Action::FocusNextPane), + }, + Some(ProtobufActionName::FocusPreviousPane) => match protobuf_action.optional_payload { + Some(_) => Err("FocusPreviousPane should not have a payload"), + None => Ok(Action::FocusPreviousPane), + }, + Some(ProtobufActionName::SwitchFocus) => match protobuf_action.optional_payload { + Some(_) => Err("SwitchFocus should not have a payload"), + None => Ok(Action::SwitchFocus), + }, + Some(ProtobufActionName::MoveFocus) => match protobuf_action.optional_payload { + Some(OptionalPayload::MoveFocusPayload(move_focus_payload)) => { + let direction: Direction = + ProtobufResizeDirection::from_i32(move_focus_payload) + .ok_or("Malformed resize direction for Action::MoveFocus")? + .try_into()?; + Ok(Action::MoveFocus(direction)) + }, + _ => Err("Wrong payload for Action::MoveFocus"), + }, + Some(ProtobufActionName::MoveFocusOrTab) => match protobuf_action.optional_payload { + Some(OptionalPayload::MoveFocusOrTabPayload(move_focus_or_tab_payload)) => { + let direction: Direction = + ProtobufResizeDirection::from_i32(move_focus_or_tab_payload) + .ok_or("Malformed resize direction for Action::MoveFocusOrTab")? + .try_into()?; + Ok(Action::MoveFocusOrTab(direction)) + }, + _ => Err("Wrong payload for Action::MoveFocusOrTab"), + }, + Some(ProtobufActionName::MovePane) => match protobuf_action.optional_payload { + Some(OptionalPayload::MovePanePayload(payload)) => { + let direction: Option = payload + .direction + .and_then(|d| ProtobufResizeDirection::from_i32(d)) + .and_then(|d| d.try_into().ok()); + Ok(Action::MovePane(direction)) + }, + _ => Err("Wrong payload for Action::MovePane"), + }, + Some(ProtobufActionName::MovePaneBackwards) => match protobuf_action.optional_payload { + Some(_) => Err("MovePaneBackwards should not have a payload"), + None => Ok(Action::MovePaneBackwards), + }, + Some(ProtobufActionName::ClearScreen) => match protobuf_action.optional_payload { + Some(_) => Err("ClearScreen should not have a payload"), + None => Ok(Action::ClearScreen), + }, + Some(ProtobufActionName::DumpScreen) => match protobuf_action.optional_payload { + Some(OptionalPayload::DumpScreenPayload(payload)) => { + let file_path = payload.file_path; + let include_scrollback = payload.include_scrollback; + Ok(Action::DumpScreen(file_path, include_scrollback)) + }, + _ => Err("Wrong payload for Action::DumpScreen"), + }, + Some(ProtobufActionName::EditScrollback) => match protobuf_action.optional_payload { + Some(_) => Err("EditScrollback should not have a payload"), + None => Ok(Action::EditScrollback), + }, + Some(ProtobufActionName::ScrollUp) => match protobuf_action.optional_payload { + Some(_) => Err("ScrollUp should not have a payload"), + None => Ok(Action::ScrollUp), + }, + Some(ProtobufActionName::ScrollDown) => match protobuf_action.optional_payload { + Some(_) => Err("ScrollDown should not have a payload"), + None => Ok(Action::ScrollDown), + }, + Some(ProtobufActionName::ScrollUpAt) => match protobuf_action.optional_payload { + Some(OptionalPayload::ScrollUpAtPayload(payload)) => { + let position = payload + .position + .ok_or("ScrollUpAtPayload must have a position")? + .try_into()?; + Ok(Action::ScrollUpAt(position)) + }, + _ => Err("Wrong payload for Action::ScrollUpAt"), + }, + Some(ProtobufActionName::ScrollDownAt) => match protobuf_action.optional_payload { + Some(OptionalPayload::ScrollDownAtPayload(payload)) => { + let position = payload + .position + .ok_or("ScrollDownAtPayload must have a position")? + .try_into()?; + Ok(Action::ScrollDownAt(position)) + }, + _ => Err("Wrong payload for Action::ScrollDownAt"), + }, + Some(ProtobufActionName::ScrollToBottom) => match protobuf_action.optional_payload { + Some(_) => Err("ScrollToBottom should not have a payload"), + None => Ok(Action::ScrollToBottom), + }, + Some(ProtobufActionName::ScrollToTop) => match protobuf_action.optional_payload { + Some(_) => Err("ScrollToTop should not have a payload"), + None => Ok(Action::ScrollToTop), + }, + Some(ProtobufActionName::PageScrollUp) => match protobuf_action.optional_payload { + Some(_) => Err("PageScrollUp should not have a payload"), + None => Ok(Action::PageScrollUp), + }, + Some(ProtobufActionName::PageScrollDown) => match protobuf_action.optional_payload { + Some(_) => Err("PageScrollDown should not have a payload"), + None => Ok(Action::PageScrollDown), + }, + Some(ProtobufActionName::HalfPageScrollUp) => match protobuf_action.optional_payload { + Some(_) => Err("HalfPageScrollUp should not have a payload"), + None => Ok(Action::HalfPageScrollUp), + }, + Some(ProtobufActionName::HalfPageScrollDown) => { + match protobuf_action.optional_payload { + Some(_) => Err("HalfPageScrollDown should not have a payload"), + None => Ok(Action::HalfPageScrollDown), + } + }, + Some(ProtobufActionName::ToggleFocusFullscreen) => { + match protobuf_action.optional_payload { + Some(_) => Err("ToggleFocusFullscreen should not have a payload"), + None => Ok(Action::ToggleFocusFullscreen), + } + }, + Some(ProtobufActionName::TogglePaneFrames) => match protobuf_action.optional_payload { + Some(_) => Err("TogglePaneFrames should not have a payload"), + None => Ok(Action::TogglePaneFrames), + }, + Some(ProtobufActionName::ToggleActiveSyncTab) => { + match protobuf_action.optional_payload { + Some(_) => Err("ToggleActiveSyncTab should not have a payload"), + None => Ok(Action::ToggleActiveSyncTab), + } + }, + Some(ProtobufActionName::NewPane) => match protobuf_action.optional_payload { + Some(OptionalPayload::NewPanePayload(payload)) => { + let direction: Option = payload + .direction + .and_then(|d| ProtobufResizeDirection::from_i32(d)) + .and_then(|d| d.try_into().ok()); + let pane_name = payload.pane_name; + Ok(Action::NewPane(direction, pane_name)) + }, + _ => Err("Wrong payload for Action::NewPane"), + }, + Some(ProtobufActionName::EditFile) => match protobuf_action.optional_payload { + Some(OptionalPayload::EditFilePayload(payload)) => { + let file_to_edit = PathBuf::from(payload.file_to_edit); + let line_number: Option = payload.line_number.map(|l| l as usize); + let cwd: Option = payload.cwd.map(|p| PathBuf::from(p)); + let direction: Option = payload + .direction + .and_then(|d| ProtobufResizeDirection::from_i32(d)) + .and_then(|d| d.try_into().ok()); + let should_float = payload.should_float; + Ok(Action::EditFile( + file_to_edit, + line_number, + cwd, + direction, + should_float, + )) + }, + _ => Err("Wrong payload for Action::NewPane"), + }, + Some(ProtobufActionName::NewFloatingPane) => match protobuf_action.optional_payload { + Some(OptionalPayload::NewFloatingPanePayload(payload)) => { + if let Some(payload) = payload.command { + let pane_name = payload.pane_name.clone(); + let run_command_action: RunCommandAction = payload.try_into()?; + Ok(Action::NewFloatingPane(Some(run_command_action), pane_name)) + } else { + Ok(Action::NewFloatingPane(None, None)) + } + }, + _ => Err("Wrong payload for Action::NewFloatingPane"), + }, + Some(ProtobufActionName::NewTiledPane) => match protobuf_action.optional_payload { + Some(OptionalPayload::NewTiledPanePayload(payload)) => { + let direction: Option = payload + .direction + .and_then(|d| ProtobufResizeDirection::from_i32(d)) + .and_then(|d| d.try_into().ok()); + if let Some(payload) = payload.command { + let pane_name = payload.pane_name.clone(); + let run_command_action: RunCommandAction = payload.try_into()?; + Ok(Action::NewTiledPane( + direction, + Some(run_command_action), + pane_name, + )) + } else { + Ok(Action::NewTiledPane(direction, None, None)) + } + }, + _ => Err("Wrong payload for Action::NewTiledPane"), + }, + Some(ProtobufActionName::TogglePaneEmbedOrFloating) => { + match protobuf_action.optional_payload { + Some(_) => Err("TogglePaneEmbedOrFloating should not have a payload"), + None => Ok(Action::TogglePaneEmbedOrFloating), + } + }, + Some(ProtobufActionName::ToggleFloatingPanes) => { + match protobuf_action.optional_payload { + Some(_) => Err("ToggleFloatingPanes should not have a payload"), + None => Ok(Action::ToggleFloatingPanes), + } + }, + Some(ProtobufActionName::CloseFocus) => match protobuf_action.optional_payload { + Some(_) => Err("CloseFocus should not have a payload"), + None => Ok(Action::CloseFocus), + }, + Some(ProtobufActionName::PaneNameInput) => match protobuf_action.optional_payload { + Some(OptionalPayload::PaneNameInputPayload(bytes)) => { + Ok(Action::PaneNameInput(bytes)) + }, + _ => Err("Wrong payload for Action::PaneNameInput"), + }, + Some(ProtobufActionName::UndoRenamePane) => match protobuf_action.optional_payload { + Some(_) => Err("UndoRenamePane should not have a payload"), + None => Ok(Action::UndoRenamePane), + }, + Some(ProtobufActionName::NewTab) => { + match protobuf_action.optional_payload { + Some(_) => Err("NewTab should not have a payload"), + None => { + // we do not serialize the layouts of this action + Ok(Action::NewTab(None, vec![], None, None, None)) + }, + } + }, + Some(ProtobufActionName::NoOp) => match protobuf_action.optional_payload { + Some(_) => Err("NoOp should not have a payload"), + None => Ok(Action::NoOp), + }, + Some(ProtobufActionName::GoToNextTab) => match protobuf_action.optional_payload { + Some(_) => Err("GoToNextTab should not have a payload"), + None => Ok(Action::GoToNextTab), + }, + Some(ProtobufActionName::GoToPreviousTab) => match protobuf_action.optional_payload { + Some(_) => Err("GoToPreviousTab should not have a payload"), + None => Ok(Action::GoToPreviousTab), + }, + Some(ProtobufActionName::CloseTab) => match protobuf_action.optional_payload { + Some(_) => Err("CloseTab should not have a payload"), + None => Ok(Action::CloseTab), + }, + Some(ProtobufActionName::GoToTab) => match protobuf_action.optional_payload { + Some(OptionalPayload::GoToTabPayload(index)) => Ok(Action::GoToTab(index)), + _ => Err("Wrong payload for Action::GoToTab"), + }, + Some(ProtobufActionName::GoToTabName) => match protobuf_action.optional_payload { + Some(OptionalPayload::GoToTabNamePayload(payload)) => { + let tab_name = payload.tab_name; + let create = payload.create; + Ok(Action::GoToTabName(tab_name, create)) + }, + _ => Err("Wrong payload for Action::GoToTabName"), + }, + Some(ProtobufActionName::ToggleTab) => match protobuf_action.optional_payload { + Some(_) => Err("ToggleTab should not have a payload"), + None => Ok(Action::ToggleTab), + }, + Some(ProtobufActionName::TabNameInput) => match protobuf_action.optional_payload { + Some(OptionalPayload::TabNameInputPayload(bytes)) => { + Ok(Action::TabNameInput(bytes)) + }, + _ => Err("Wrong payload for Action::TabNameInput"), + }, + Some(ProtobufActionName::UndoRenameTab) => match protobuf_action.optional_payload { + Some(_) => Err("UndoRenameTab should not have a payload"), + None => Ok(Action::UndoRenameTab), + }, + Some(ProtobufActionName::Run) => match protobuf_action.optional_payload { + Some(OptionalPayload::RunPayload(run_command_action)) => { + let run_command_action = run_command_action.try_into()?; + Ok(Action::Run(run_command_action)) + }, + _ => Err("Wrong payload for Action::Run"), + }, + Some(ProtobufActionName::Detach) => match protobuf_action.optional_payload { + Some(_) => Err("Detach should not have a payload"), + None => Ok(Action::Detach), + }, + Some(ProtobufActionName::LeftClick) => match protobuf_action.optional_payload { + Some(OptionalPayload::LeftClickPayload(payload)) => { + let position = payload.try_into()?; + Ok(Action::LeftClick(position)) + }, + _ => Err("Wrong payload for Action::LeftClick"), + }, + Some(ProtobufActionName::RightClick) => match protobuf_action.optional_payload { + Some(OptionalPayload::RightClickPayload(payload)) => { + let position = payload.try_into()?; + Ok(Action::RightClick(position)) + }, + _ => Err("Wrong payload for Action::RightClick"), + }, + Some(ProtobufActionName::MiddleClick) => match protobuf_action.optional_payload { + Some(OptionalPayload::MiddleClickPayload(payload)) => { + let position = payload.try_into()?; + Ok(Action::MiddleClick(position)) + }, + _ => Err("Wrong payload for Action::MiddleClick"), + }, + Some(ProtobufActionName::LaunchOrFocusPlugin) => { + match protobuf_action.optional_payload { + Some(OptionalPayload::LaunchOrFocusPluginPayload(payload)) => { + let run_plugin_location = + RunPluginLocation::parse(&payload.plugin_url, None) + .map_err(|_| "Malformed LaunchOrFocusPlugin payload")?; + let configuration: PluginUserConfiguration = payload + .plugin_configuration + .and_then(|p| PluginUserConfiguration::try_from(p).ok()) + .unwrap_or_default(); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: run_plugin_location, + configuration, + }; + let should_float = payload.should_float; + Ok(Action::LaunchOrFocusPlugin(run_plugin, should_float)) + }, + _ => Err("Wrong payload for Action::LaunchOrFocusPlugin"), + } + }, + Some(ProtobufActionName::LeftMouseRelease) => match protobuf_action.optional_payload { + Some(OptionalPayload::LeftMouseReleasePayload(payload)) => { + let position = payload.try_into()?; + Ok(Action::LeftMouseRelease(position)) + }, + _ => Err("Wrong payload for Action::LeftMouseRelease"), + }, + Some(ProtobufActionName::RightMouseRelease) => match protobuf_action.optional_payload { + Some(OptionalPayload::RightMouseReleasePayload(payload)) => { + let position = payload.try_into()?; + Ok(Action::RightMouseRelease(position)) + }, + _ => Err("Wrong payload for Action::RightMouseRelease"), + }, + Some(ProtobufActionName::MiddleMouseRelease) => { + match protobuf_action.optional_payload { + Some(OptionalPayload::MiddleMouseReleasePayload(payload)) => { + let position = payload.try_into()?; + Ok(Action::MiddleMouseRelease(position)) + }, + _ => Err("Wrong payload for Action::MiddleMouseRelease"), + } + }, + Some(ProtobufActionName::MouseHoldLeft) => match protobuf_action.optional_payload { + Some(OptionalPayload::MouseHoldLeftPayload(payload)) => { + let position = payload.try_into()?; + Ok(Action::MouseHoldLeft(position)) + }, + _ => Err("Wrong payload for Action::MouseHoldLeft"), + }, + Some(ProtobufActionName::MouseHoldRight) => match protobuf_action.optional_payload { + Some(OptionalPayload::MouseHoldRightPayload(payload)) => { + let position = payload.try_into()?; + Ok(Action::MouseHoldRight(position)) + }, + _ => Err("Wrong payload for Action::MouseHoldRight"), + }, + Some(ProtobufActionName::MouseHoldMiddle) => match protobuf_action.optional_payload { + Some(OptionalPayload::MouseHoldMiddlePayload(payload)) => { + let position = payload.try_into()?; + Ok(Action::MouseHoldMiddle(position)) + }, + _ => Err("Wrong payload for Action::MouseHoldMiddle"), + }, + Some(ProtobufActionName::SearchInput) => match protobuf_action.optional_payload { + Some(OptionalPayload::SearchInputPayload(payload)) => { + Ok(Action::SearchInput(payload)) + }, + _ => Err("Wrong payload for Action::SearchInput"), + }, + Some(ProtobufActionName::Search) => match protobuf_action.optional_payload { + Some(OptionalPayload::SearchPayload(search_direction)) => Ok(Action::Search( + ProtobufSearchDirection::from_i32(search_direction) + .ok_or("Malformed payload for Action::Search")? + .try_into()?, + )), + _ => Err("Wrong payload for Action::Search"), + }, + Some(ProtobufActionName::SearchToggleOption) => { + match protobuf_action.optional_payload { + Some(OptionalPayload::SearchToggleOptionPayload(search_option)) => { + Ok(Action::SearchToggleOption( + ProtobufSearchOption::from_i32(search_option) + .ok_or("Malformed payload for Action::SearchToggleOption")? + .try_into()?, + )) + }, + _ => Err("Wrong payload for Action::SearchToggleOption"), + } + }, + Some(ProtobufActionName::ToggleMouseMode) => match protobuf_action.optional_payload { + Some(_) => Err("ToggleMouseMode should not have a payload"), + None => Ok(Action::ToggleMouseMode), + }, + Some(ProtobufActionName::PreviousSwapLayout) => { + match protobuf_action.optional_payload { + Some(_) => Err("PreviousSwapLayout should not have a payload"), + None => Ok(Action::PreviousSwapLayout), + } + }, + Some(ProtobufActionName::NextSwapLayout) => match protobuf_action.optional_payload { + Some(_) => Err("NextSwapLayout should not have a payload"), + None => Ok(Action::NextSwapLayout), + }, + Some(ProtobufActionName::QueryTabNames) => match protobuf_action.optional_payload { + Some(_) => Err("QueryTabNames should not have a payload"), + None => Ok(Action::QueryTabNames), + }, + Some(ProtobufActionName::NewTiledPluginPane) => { + match protobuf_action.optional_payload { + Some(OptionalPayload::NewTiledPluginPanePayload(payload)) => { + let run_plugin_location = + RunPluginLocation::parse(&payload.plugin_url, None) + .map_err(|_| "Malformed NewTiledPluginPane payload")?; + let run_plugin = RunPlugin { + location: run_plugin_location, + _allow_exec_host_cmd: false, + configuration: PluginUserConfiguration::default(), + }; + let pane_name = payload.pane_name; + Ok(Action::NewTiledPluginPane(run_plugin, pane_name)) + }, + _ => Err("Wrong payload for Action::NewTiledPluginPane"), + } + }, + Some(ProtobufActionName::NewFloatingPluginPane) => { + match protobuf_action.optional_payload { + Some(OptionalPayload::NewFloatingPluginPanePayload(payload)) => { + let run_plugin_location = + RunPluginLocation::parse(&payload.plugin_url, None) + .map_err(|_| "Malformed NewTiledPluginPane payload")?; + let run_plugin = RunPlugin { + location: run_plugin_location, + _allow_exec_host_cmd: false, + configuration: PluginUserConfiguration::default(), + }; + let pane_name = payload.pane_name; + Ok(Action::NewFloatingPluginPane(run_plugin, pane_name)) + }, + _ => Err("Wrong payload for Action::MiddleClick"), + } + }, + Some(ProtobufActionName::StartOrReloadPlugin) => { + match protobuf_action.optional_payload { + Some(OptionalPayload::StartOrReloadPluginPayload(payload)) => { + let run_plugin_location = RunPluginLocation::parse(&payload, None) + .map_err(|_| "Malformed StartOrReloadPluginPayload payload")?; + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: run_plugin_location, + configuration: PluginUserConfiguration::default(), + }; + Ok(Action::StartOrReloadPlugin(run_plugin)) + }, + _ => Err("Wrong payload for Action::StartOrReloadPlugin"), + } + }, + Some(ProtobufActionName::CloseTerminalPane) => match protobuf_action.optional_payload { + Some(OptionalPayload::CloseTerminalPanePayload(payload)) => { + Ok(Action::CloseTerminalPane(payload)) + }, + _ => Err("Wrong payload for Action::CloseTerminalPane"), + }, + Some(ProtobufActionName::ClosePluginPane) => match protobuf_action.optional_payload { + Some(OptionalPayload::ClosePluginPanePayload(payload)) => { + Ok(Action::ClosePluginPane(payload)) + }, + _ => Err("Wrong payload for Action::ClosePluginPane"), + }, + Some(ProtobufActionName::FocusTerminalPaneWithId) => { + match protobuf_action.optional_payload { + Some(OptionalPayload::FocusTerminalPaneWithIdPayload(payload)) => { + let terminal_pane_id = payload.pane_id; + let should_float_if_hidden = payload.should_float_if_hidden; + Ok(Action::FocusTerminalPaneWithId( + terminal_pane_id, + should_float_if_hidden, + )) + }, + _ => Err("Wrong payload for Action::FocusTerminalPaneWithId"), + } + }, + Some(ProtobufActionName::FocusPluginPaneWithId) => { + match protobuf_action.optional_payload { + Some(OptionalPayload::FocusPluginPaneWithIdPayload(payload)) => { + let plugin_pane_id = payload.pane_id; + let should_float_if_hidden = payload.should_float_if_hidden; + Ok(Action::FocusPluginPaneWithId( + plugin_pane_id, + should_float_if_hidden, + )) + }, + _ => Err("Wrong payload for Action::FocusPluginPaneWithId"), + } + }, + Some(ProtobufActionName::RenameTerminalPane) => { + match protobuf_action.optional_payload { + Some(OptionalPayload::RenameTerminalPanePayload(payload)) => { + let terminal_pane_id = payload.id; + let new_pane_name = payload.name; + Ok(Action::RenameTerminalPane(terminal_pane_id, new_pane_name)) + }, + _ => Err("Wrong payload for Action::RenameTerminalPane"), + } + }, + Some(ProtobufActionName::RenamePluginPane) => match protobuf_action.optional_payload { + Some(OptionalPayload::RenamePluginPanePayload(payload)) => { + let plugin_pane_id = payload.id; + let new_pane_name = payload.name; + Ok(Action::RenamePluginPane(plugin_pane_id, new_pane_name)) + }, + _ => Err("Wrong payload for Action::RenamePluginPane"), + }, + Some(ProtobufActionName::RenameTab) => match protobuf_action.optional_payload { + Some(OptionalPayload::RenameTabPayload(payload)) => { + let tab_index = payload.id; + let new_tab_name = payload.name; + Ok(Action::RenameTab(tab_index, new_tab_name)) + }, + _ => Err("Wrong payload for Action::RenameTab"), + }, + Some(ProtobufActionName::BreakPane) => match protobuf_action.optional_payload { + Some(_) => Err("BreakPane should not have a payload"), + None => Ok(Action::BreakPane), + }, + Some(ProtobufActionName::BreakPaneRight) => match protobuf_action.optional_payload { + Some(_) => Err("BreakPaneRight should not have a payload"), + None => Ok(Action::BreakPaneRight), + }, + Some(ProtobufActionName::BreakPaneLeft) => match protobuf_action.optional_payload { + Some(_) => Err("BreakPaneLeft should not have a payload"), + None => Ok(Action::BreakPaneLeft), + }, + _ => Err("Unknown Action"), + } + } +} + +impl TryFrom for ProtobufAction { + type Error = &'static str; + fn try_from(action: Action) -> Result { + match action { + Action::Quit => Ok(ProtobufAction { + name: ProtobufActionName::Quit as i32, + optional_payload: None, + }), + Action::Write(bytes) => Ok(ProtobufAction { + name: ProtobufActionName::Write as i32, + optional_payload: Some(OptionalPayload::WritePayload(WritePayload { + bytes_to_write: bytes, + })), + }), + Action::WriteChars(chars_to_write) => Ok(ProtobufAction { + name: ProtobufActionName::WriteChars as i32, + optional_payload: Some(OptionalPayload::WriteCharsPayload(WriteCharsPayload { + chars: chars_to_write, + })), + }), + Action::SwitchToMode(input_mode) => { + let input_mode: ProtobufInputMode = input_mode.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::SwitchToMode as i32, + optional_payload: Some(OptionalPayload::SwitchToModePayload( + SwitchToModePayload { + input_mode: input_mode as i32, + }, + )), + }) + }, + Action::SwitchModeForAllClients(input_mode) => { + let input_mode: ProtobufInputMode = input_mode.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::SwitchModeForAllClients as i32, + optional_payload: Some(OptionalPayload::SwitchModeForAllClientsPayload( + SwitchToModePayload { + input_mode: input_mode as i32, + }, + )), + }) + }, + Action::Resize(resize, direction) => { + let mut resize: ProtobufResize = resize.try_into()?; + resize.direction = direction.and_then(|d| { + let resize_direction: ProtobufResizeDirection = d.try_into().ok()?; + Some(resize_direction as i32) + }); + Ok(ProtobufAction { + name: ProtobufActionName::Resize as i32, + optional_payload: Some(OptionalPayload::ResizePayload(resize)), + }) + }, + Action::FocusNextPane => Ok(ProtobufAction { + name: ProtobufActionName::FocusNextPane as i32, + optional_payload: None, + }), + Action::FocusPreviousPane => Ok(ProtobufAction { + name: ProtobufActionName::FocusPreviousPane as i32, + optional_payload: None, + }), + Action::SwitchFocus => Ok(ProtobufAction { + name: ProtobufActionName::SwitchFocus as i32, + optional_payload: None, + }), + Action::MoveFocus(direction) => { + let direction: ProtobufResizeDirection = direction.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::MoveFocus as i32, + optional_payload: Some(OptionalPayload::MoveFocusPayload(direction as i32)), + }) + }, + Action::MoveFocusOrTab(direction) => { + let direction: ProtobufResizeDirection = direction.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::MoveFocusOrTab as i32, + optional_payload: Some(OptionalPayload::MoveFocusOrTabPayload( + direction as i32, + )), + }) + }, + Action::MovePane(direction) => { + let direction = direction.and_then(|direction| { + let protobuf_direction: ProtobufResizeDirection = direction.try_into().ok()?; + Some(protobuf_direction as i32) + }); + Ok(ProtobufAction { + name: ProtobufActionName::MovePane as i32, + optional_payload: Some(OptionalPayload::MovePanePayload(MovePanePayload { + direction, + })), + }) + }, + Action::MovePaneBackwards => Ok(ProtobufAction { + name: ProtobufActionName::MovePaneBackwards as i32, + optional_payload: None, + }), + Action::ClearScreen => Ok(ProtobufAction { + name: ProtobufActionName::ClearScreen as i32, + optional_payload: None, + }), + Action::DumpScreen(file_path, include_scrollback) => Ok(ProtobufAction { + name: ProtobufActionName::DumpScreen as i32, + optional_payload: Some(OptionalPayload::DumpScreenPayload(DumpScreenPayload { + file_path, + include_scrollback, + })), + }), + Action::EditScrollback => Ok(ProtobufAction { + name: ProtobufActionName::EditScrollback as i32, + optional_payload: None, + }), + Action::ScrollUp => Ok(ProtobufAction { + name: ProtobufActionName::ScrollUp as i32, + optional_payload: None, + }), + Action::ScrollUpAt(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::ScrollUpAt as i32, + optional_payload: Some(OptionalPayload::ScrollUpAtPayload(ScrollAtPayload { + position: Some(position), + })), + }) + }, + Action::ScrollDown => Ok(ProtobufAction { + name: ProtobufActionName::ScrollDown as i32, + optional_payload: None, + }), + Action::ScrollDownAt(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::ScrollDownAt as i32, + optional_payload: Some(OptionalPayload::ScrollDownAtPayload(ScrollAtPayload { + position: Some(position), + })), + }) + }, + Action::ScrollToBottom => Ok(ProtobufAction { + name: ProtobufActionName::ScrollToBottom as i32, + optional_payload: None, + }), + Action::ScrollToTop => Ok(ProtobufAction { + name: ProtobufActionName::ScrollToTop as i32, + optional_payload: None, + }), + Action::PageScrollUp => Ok(ProtobufAction { + name: ProtobufActionName::PageScrollUp as i32, + optional_payload: None, + }), + Action::PageScrollDown => Ok(ProtobufAction { + name: ProtobufActionName::PageScrollDown as i32, + optional_payload: None, + }), + Action::HalfPageScrollUp => Ok(ProtobufAction { + name: ProtobufActionName::HalfPageScrollUp as i32, + optional_payload: None, + }), + Action::HalfPageScrollDown => Ok(ProtobufAction { + name: ProtobufActionName::HalfPageScrollDown as i32, + optional_payload: None, + }), + Action::ToggleFocusFullscreen => Ok(ProtobufAction { + name: ProtobufActionName::ToggleFocusFullscreen as i32, + optional_payload: None, + }), + Action::TogglePaneFrames => Ok(ProtobufAction { + name: ProtobufActionName::TogglePaneFrames as i32, + optional_payload: None, + }), + Action::ToggleActiveSyncTab => Ok(ProtobufAction { + name: ProtobufActionName::ToggleActiveSyncTab as i32, + optional_payload: None, + }), + Action::NewPane(direction, new_pane_name) => { + let direction = direction.and_then(|direction| { + let protobuf_direction: ProtobufResizeDirection = direction.try_into().ok()?; + Some(protobuf_direction as i32) + }); + Ok(ProtobufAction { + name: ProtobufActionName::NewPane as i32, + optional_payload: Some(OptionalPayload::NewPanePayload(NewPanePayload { + direction, + pane_name: new_pane_name, + })), + }) + }, + Action::EditFile(path_to_file, line_number, cwd, direction, should_float) => { + let file_to_edit = path_to_file.display().to_string(); + let cwd = cwd.map(|cwd| cwd.display().to_string()); + let direction: Option = direction + .and_then(|d| ProtobufResizeDirection::try_from(d).ok()) + .map(|d| d as i32); + let line_number = line_number.map(|l| l as u32); + Ok(ProtobufAction { + name: ProtobufActionName::EditFile as i32, + optional_payload: Some(OptionalPayload::EditFilePayload(EditFilePayload { + file_to_edit, + line_number, + should_float, + direction, + cwd, + })), + }) + }, + Action::NewFloatingPane(run_command_action, pane_name) => { + let command = run_command_action.and_then(|r| { + let mut protobuf_run_command_action: ProtobufRunCommandAction = + r.try_into().ok()?; + protobuf_run_command_action.pane_name = pane_name; + Some(protobuf_run_command_action) + }); + Ok(ProtobufAction { + name: ProtobufActionName::NewFloatingPane as i32, + optional_payload: Some(OptionalPayload::NewFloatingPanePayload( + NewFloatingPanePayload { command }, + )), + }) + }, + Action::NewTiledPane(direction, run_command_action, pane_name) => { + let direction = direction.and_then(|direction| { + let protobuf_direction: ProtobufResizeDirection = direction.try_into().ok()?; + Some(protobuf_direction as i32) + }); + let command = run_command_action.and_then(|r| { + let mut protobuf_run_command_action: ProtobufRunCommandAction = + r.try_into().ok()?; + let pane_name = pane_name.and_then(|n| n.try_into().ok()); + protobuf_run_command_action.pane_name = pane_name; + Some(protobuf_run_command_action) + }); + Ok(ProtobufAction { + name: ProtobufActionName::NewTiledPane as i32, + optional_payload: Some(OptionalPayload::NewTiledPanePayload( + NewTiledPanePayload { direction, command }, + )), + }) + }, + Action::TogglePaneEmbedOrFloating => Ok(ProtobufAction { + name: ProtobufActionName::TogglePaneEmbedOrFloating as i32, + optional_payload: None, + }), + Action::ToggleFloatingPanes => Ok(ProtobufAction { + name: ProtobufActionName::ToggleFloatingPanes as i32, + optional_payload: None, + }), + Action::CloseFocus => Ok(ProtobufAction { + name: ProtobufActionName::CloseFocus as i32, + optional_payload: None, + }), + Action::PaneNameInput(bytes) => Ok(ProtobufAction { + name: ProtobufActionName::PaneNameInput as i32, + optional_payload: Some(OptionalPayload::PaneNameInputPayload(bytes)), + }), + Action::UndoRenamePane => Ok(ProtobufAction { + name: ProtobufActionName::UndoRenamePane as i32, + optional_payload: None, + }), + Action::NewTab(..) => { + // we do not serialize the various newtab payloads + Ok(ProtobufAction { + name: ProtobufActionName::NewTab as i32, + optional_payload: None, + }) + }, + Action::GoToNextTab => Ok(ProtobufAction { + name: ProtobufActionName::GoToNextTab as i32, + optional_payload: None, + }), + Action::GoToPreviousTab => Ok(ProtobufAction { + name: ProtobufActionName::GoToPreviousTab as i32, + optional_payload: None, + }), + Action::CloseTab => Ok(ProtobufAction { + name: ProtobufActionName::CloseTab as i32, + optional_payload: None, + }), + Action::GoToTab(tab_index) => Ok(ProtobufAction { + name: ProtobufActionName::GoToTab as i32, + optional_payload: Some(OptionalPayload::GoToTabPayload(tab_index)), + }), + Action::GoToTabName(tab_name, create) => Ok(ProtobufAction { + name: ProtobufActionName::GoToTabName as i32, + optional_payload: Some(OptionalPayload::GoToTabNamePayload(GoToTabNamePayload { + tab_name, + create, + })), + }), + Action::ToggleTab => Ok(ProtobufAction { + name: ProtobufActionName::ToggleTab as i32, + optional_payload: None, + }), + Action::TabNameInput(bytes) => Ok(ProtobufAction { + name: ProtobufActionName::TabNameInput as i32, + optional_payload: Some(OptionalPayload::TabNameInputPayload(bytes)), + }), + Action::UndoRenameTab => Ok(ProtobufAction { + name: ProtobufActionName::UndoRenameTab as i32, + optional_payload: None, + }), + Action::Run(run_command_action) => { + let run_command_action: ProtobufRunCommandAction = run_command_action.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::Run as i32, + optional_payload: Some(OptionalPayload::RunPayload(run_command_action)), + }) + }, + Action::Detach => Ok(ProtobufAction { + name: ProtobufActionName::Detach as i32, + optional_payload: None, + }), + Action::LeftClick(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::LeftClick as i32, + optional_payload: Some(OptionalPayload::LeftClickPayload(position)), + }) + }, + Action::RightClick(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::RightClick as i32, + optional_payload: Some(OptionalPayload::RightClickPayload(position)), + }) + }, + Action::MiddleClick(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::MiddleClick as i32, + optional_payload: Some(OptionalPayload::MiddleClickPayload(position)), + }) + }, + Action::LaunchOrFocusPlugin(run_plugin, should_float) => { + let url: Url = Url::from(&run_plugin.location); + Ok(ProtobufAction { + name: ProtobufActionName::LaunchOrFocusPlugin as i32, + optional_payload: Some(OptionalPayload::LaunchOrFocusPluginPayload( + LaunchOrFocusPluginPayload { + plugin_url: url.into(), + should_float, + plugin_configuration: Some(run_plugin.configuration.try_into()?), + }, + )), + }) + }, + Action::LeftMouseRelease(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::LeftMouseRelease as i32, + optional_payload: Some(OptionalPayload::LeftMouseReleasePayload(position)), + }) + }, + Action::RightMouseRelease(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::RightMouseRelease as i32, + optional_payload: Some(OptionalPayload::RightMouseReleasePayload(position)), + }) + }, + Action::MiddleMouseRelease(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::MiddleMouseRelease as i32, + optional_payload: Some(OptionalPayload::MiddleMouseReleasePayload(position)), + }) + }, + Action::MouseHoldLeft(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::MouseHoldLeft as i32, + optional_payload: Some(OptionalPayload::MouseHoldLeftPayload(position)), + }) + }, + Action::MouseHoldRight(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::MouseHoldRight as i32, + optional_payload: Some(OptionalPayload::MouseHoldRightPayload(position)), + }) + }, + Action::MouseHoldMiddle(position) => { + let position: ProtobufPosition = position.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::MouseHoldMiddle as i32, + optional_payload: Some(OptionalPayload::MouseHoldMiddlePayload(position)), + }) + }, + Action::SearchInput(bytes) => Ok(ProtobufAction { + name: ProtobufActionName::SearchInput as i32, + optional_payload: Some(OptionalPayload::SearchInputPayload(bytes)), + }), + Action::Search(search_direction) => { + let search_direction: ProtobufSearchDirection = search_direction.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::Search as i32, + optional_payload: Some(OptionalPayload::SearchPayload(search_direction as i32)), + }) + }, + Action::SearchToggleOption(search_option) => { + let search_option: ProtobufSearchOption = search_option.try_into()?; + Ok(ProtobufAction { + name: ProtobufActionName::SearchToggleOption as i32, + optional_payload: Some(OptionalPayload::SearchToggleOptionPayload( + search_option as i32, + )), + }) + }, + Action::ToggleMouseMode => Ok(ProtobufAction { + name: ProtobufActionName::ToggleMouseMode as i32, + optional_payload: None, + }), + Action::PreviousSwapLayout => Ok(ProtobufAction { + name: ProtobufActionName::PreviousSwapLayout as i32, + optional_payload: None, + }), + Action::NextSwapLayout => Ok(ProtobufAction { + name: ProtobufActionName::NextSwapLayout as i32, + optional_payload: None, + }), + Action::QueryTabNames => Ok(ProtobufAction { + name: ProtobufActionName::QueryTabNames as i32, + optional_payload: None, + }), + Action::NewTiledPluginPane(run_plugin, pane_name) => { + let plugin_url: Url = Url::from(&run_plugin.location); + Ok(ProtobufAction { + name: ProtobufActionName::NewTiledPluginPane as i32, + optional_payload: Some(OptionalPayload::NewTiledPluginPanePayload( + NewPluginPanePayload { + plugin_url: plugin_url.into(), + pane_name, + }, + )), + }) + }, + Action::NewFloatingPluginPane(run_plugin, pane_name) => { + let plugin_url: Url = Url::from(&run_plugin.location); + Ok(ProtobufAction { + name: ProtobufActionName::NewFloatingPluginPane as i32, + optional_payload: Some(OptionalPayload::NewFloatingPluginPanePayload( + NewPluginPanePayload { + plugin_url: plugin_url.into(), + pane_name, + }, + )), + }) + }, + Action::StartOrReloadPlugin(run_plugin) => { + let plugin_url: Url = Url::from(&run_plugin.location); + Ok(ProtobufAction { + name: ProtobufActionName::StartOrReloadPlugin as i32, + optional_payload: Some(OptionalPayload::StartOrReloadPluginPayload( + plugin_url.into(), + )), + }) + }, + Action::CloseTerminalPane(terminal_pane_id) => Ok(ProtobufAction { + name: ProtobufActionName::CloseTerminalPane as i32, + optional_payload: Some(OptionalPayload::CloseTerminalPanePayload(terminal_pane_id)), + }), + Action::ClosePluginPane(plugin_pane_id) => Ok(ProtobufAction { + name: ProtobufActionName::ClosePluginPane as i32, + optional_payload: Some(OptionalPayload::ClosePluginPanePayload(plugin_pane_id)), + }), + Action::FocusTerminalPaneWithId(terminal_pane_id, should_float_if_hidden) => { + Ok(ProtobufAction { + name: ProtobufActionName::FocusTerminalPaneWithId as i32, + optional_payload: Some(OptionalPayload::FocusTerminalPaneWithIdPayload( + PaneIdAndShouldFloat { + pane_id: terminal_pane_id, + should_float_if_hidden, + }, + )), + }) + }, + Action::FocusPluginPaneWithId(plugin_pane_id, should_float_if_hidden) => { + Ok(ProtobufAction { + name: ProtobufActionName::FocusPluginPaneWithId as i32, + optional_payload: Some(OptionalPayload::FocusPluginPaneWithIdPayload( + PaneIdAndShouldFloat { + pane_id: plugin_pane_id, + should_float_if_hidden, + }, + )), + }) + }, + Action::RenameTerminalPane(terminal_pane_id, new_name) => Ok(ProtobufAction { + name: ProtobufActionName::RenameTerminalPane as i32, + optional_payload: Some(OptionalPayload::RenameTerminalPanePayload(IdAndName { + name: new_name, + id: terminal_pane_id, + })), + }), + Action::RenamePluginPane(plugin_pane_id, new_name) => Ok(ProtobufAction { + name: ProtobufActionName::RenamePluginPane as i32, + optional_payload: Some(OptionalPayload::RenamePluginPanePayload(IdAndName { + name: new_name, + id: plugin_pane_id, + })), + }), + Action::RenameTab(tab_index, new_name) => Ok(ProtobufAction { + name: ProtobufActionName::RenameTab as i32, + optional_payload: Some(OptionalPayload::RenameTabPayload(IdAndName { + name: new_name, + id: tab_index, + })), + }), + Action::BreakPane => Ok(ProtobufAction { + name: ProtobufActionName::BreakPane as i32, + optional_payload: None, + }), + Action::BreakPaneRight => Ok(ProtobufAction { + name: ProtobufActionName::BreakPaneRight as i32, + optional_payload: None, + }), + Action::BreakPaneLeft => Ok(ProtobufAction { + name: ProtobufActionName::BreakPaneLeft as i32, + optional_payload: None, + }), + Action::NoOp + | Action::Confirm + | Action::Deny + | Action::Copy + | Action::SkipConfirm(..) => Err("Unsupported action"), + } + } +} + +impl TryFrom for SearchOption { + type Error = &'static str; + fn try_from(protobuf_search_option: ProtobufSearchOption) -> Result { + match protobuf_search_option { + ProtobufSearchOption::CaseSensitivity => Ok(SearchOption::CaseSensitivity), + ProtobufSearchOption::WholeWord => Ok(SearchOption::WholeWord), + ProtobufSearchOption::Wrap => Ok(SearchOption::Wrap), + } + } +} + +impl TryFrom for ProtobufSearchOption { + type Error = &'static str; + fn try_from(search_option: SearchOption) -> Result { + match search_option { + SearchOption::CaseSensitivity => Ok(ProtobufSearchOption::CaseSensitivity), + SearchOption::WholeWord => Ok(ProtobufSearchOption::WholeWord), + SearchOption::Wrap => Ok(ProtobufSearchOption::Wrap), + } + } +} + +impl TryFrom for SearchDirection { + type Error = &'static str; + fn try_from(protobuf_search_direction: ProtobufSearchDirection) -> Result { + match protobuf_search_direction { + ProtobufSearchDirection::Up => Ok(SearchDirection::Up), + ProtobufSearchDirection::Down => Ok(SearchDirection::Down), + } + } +} + +impl TryFrom for ProtobufSearchDirection { + type Error = &'static str; + fn try_from(search_direction: SearchDirection) -> Result { + match search_direction { + SearchDirection::Up => Ok(ProtobufSearchDirection::Up), + SearchDirection::Down => Ok(ProtobufSearchDirection::Down), + } + } +} + +impl TryFrom for RunCommandAction { + type Error = &'static str; + fn try_from( + protobuf_run_command_action: ProtobufRunCommandAction, + ) -> Result { + let command = PathBuf::from(protobuf_run_command_action.command); + let args: Vec = protobuf_run_command_action.args; + let cwd: Option = protobuf_run_command_action.cwd.map(|c| PathBuf::from(c)); + let direction: Option = protobuf_run_command_action + .direction + .and_then(|d| ProtobufResizeDirection::from_i32(d)) + .and_then(|d| d.try_into().ok()); + let hold_on_close = protobuf_run_command_action.hold_on_close; + let hold_on_start = protobuf_run_command_action.hold_on_start; + Ok(RunCommandAction { + command, + args, + cwd, + direction, + hold_on_close, + hold_on_start, + }) + } +} + +impl TryFrom for ProtobufRunCommandAction { + type Error = &'static str; + fn try_from(run_command_action: RunCommandAction) -> Result { + let command = run_command_action.command.display().to_string(); + let args: Vec = run_command_action.args; + let cwd = run_command_action.cwd.map(|c| c.display().to_string()); + let direction = run_command_action.direction.and_then(|p| { + let direction: ProtobufResizeDirection = p.try_into().ok()?; + Some(direction as i32) + }); + let hold_on_close = run_command_action.hold_on_close; + let hold_on_start = run_command_action.hold_on_start; + Ok(ProtobufRunCommandAction { + command, + args, + cwd, + direction, + hold_on_close, + hold_on_start, + pane_name: None, + }) + } +} + +impl TryFrom for Position { + type Error = &'static str; + fn try_from(protobuf_position: ProtobufPosition) -> Result { + Ok(Position::new( + protobuf_position.line as i32, + protobuf_position.column as u16, + )) + } +} + +impl TryFrom for ProtobufPosition { + type Error = &'static str; + fn try_from(position: Position) -> Result { + Ok(ProtobufPosition { + line: position.line.0 as i64, + column: position.column.0 as i64, + }) + } +} + +impl TryFrom for PluginUserConfiguration { + type Error = &'static str; + fn try_from(plugin_configuration: ProtobufPluginConfiguration) -> Result { + let mut converted = BTreeMap::new(); + for name_and_value in plugin_configuration.name_and_value { + converted.insert(name_and_value.name, name_and_value.value); + } + Ok(PluginUserConfiguration::new(converted)) + } +} + +impl TryFrom for ProtobufPluginConfiguration { + type Error = &'static str; + fn try_from(plugin_configuration: PluginUserConfiguration) -> Result { + let mut converted = vec![]; + for (name, value) in plugin_configuration.inner() { + let name_and_value = ProtobufNameAndValue { + name: name.to_owned(), + value: value.to_owned(), + }; + converted.push(name_and_value); + } + Ok(ProtobufPluginConfiguration { + name_and_value: converted, + }) + } +} + +impl TryFrom<&ProtobufPluginConfiguration> for BTreeMap { + type Error = &'static str; + fn try_from(plugin_configuration: &ProtobufPluginConfiguration) -> Result { + let mut converted = BTreeMap::new(); + for name_and_value in &plugin_configuration.name_and_value { + converted.insert( + name_and_value.name.to_owned(), + name_and_value.value.to_owned(), + ); + } + Ok(converted) + } +} diff --git a/zellij-utils/src/plugin_api/command.proto b/zellij-utils/src/plugin_api/command.proto new file mode 100644 index 0000000000..70402f25a5 --- /dev/null +++ b/zellij-utils/src/plugin_api/command.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package api.command; + +message Command { + string path = 1; + repeated string args = 2; + optional string cwd = 3; +} diff --git a/zellij-utils/src/plugin_api/command.rs b/zellij-utils/src/plugin_api/command.rs new file mode 100644 index 0000000000..2f8bf3005e --- /dev/null +++ b/zellij-utils/src/plugin_api/command.rs @@ -0,0 +1,26 @@ +pub use super::generated_api::api::command::Command as ProtobufCommand; +use crate::data::CommandToRun; + +use std::convert::TryFrom; +use std::path::PathBuf; + +impl TryFrom for CommandToRun { + type Error = &'static str; + fn try_from(protobuf_command: ProtobufCommand) -> Result { + let path = PathBuf::from(protobuf_command.path); + let args = protobuf_command.args; + let cwd = protobuf_command.cwd.map(|c| PathBuf::from(c)); + Ok(CommandToRun { path, args, cwd }) + } +} + +impl TryFrom for ProtobufCommand { + type Error = &'static str; + fn try_from(command_to_run: CommandToRun) -> Result { + Ok(ProtobufCommand { + path: command_to_run.path.display().to_string(), + args: command_to_run.args, + cwd: command_to_run.cwd.map(|c| c.display().to_string()), + }) + } +} diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto new file mode 100644 index 0000000000..95928ae3b3 --- /dev/null +++ b/zellij-utils/src/plugin_api/event.proto @@ -0,0 +1,162 @@ +syntax = "proto3"; + +import "input_mode.proto"; +import "key.proto"; +import "style.proto"; +import "action.proto"; + +package api.event; + +enum EventType { + /// The input mode or relevant metadata changed + ModeUpdate = 0; + /// The tab state in the app was changed + TabUpdate = 1; + /// The pane state in the app was changed + PaneUpdate = 2; + /// A key was pressed while the user is focused on this plugin's pane + Key = 3; + /// A mouse event happened while the user is focused on this plugin's pane + Mouse = 4; + /// A timer expired set by the `set_timeout` method exported by `zellij-tile`. + Timer = 5; + /// Text was copied to the clipboard anywhere in the app + CopyToClipboard = 6; + /// Failed to copy text to clipboard anywhere in the app + SystemClipboardFailure = 7; + /// Input was received anywhere in the app + InputReceived = 8; + /// This plugin became visible or invisible + Visible = 9; + /// A message from one of the plugin's workers + CustomMessage = 10; + /// A file was created somewhere in the Zellij CWD folder + FileSystemCreate = 11; + /// A file was accessed somewhere in the Zellij CWD folder + FileSystemRead = 12; + /// A file was modified somewhere in the Zellij CWD folder + FileSystemUpdate = 13; + /// A file was deleted somewhere in the Zellij CWD folder + FileSystemDelete = 14; +} + +message EventNameList { + repeated EventType event_types = 1; +} + +message Event { + EventType name = 1; + oneof payload { + ModeUpdatePayload mode_update_payload = 2; + TabUpdatePayload tab_update_payload = 3; + PaneUpdatePayload pane_update_payload = 4; + key.Key key_payload = 5; + MouseEventPayload mouse_event_payload = 6; + float timer_payload = 7; + CopyDestination copy_to_clipboard_payload = 8; + bool visible_payload = 9; + CustomMessagePayload custom_message_payload = 10; + FileListPayload file_list_payload = 11; + } +} + +message FileListPayload { + repeated string paths = 1; +} + +message CustomMessagePayload { + string message_name = 1; + string payload = 2; +} + +enum CopyDestination { + Command = 0; + Primary = 1; + System = 2; +} + +message MouseEventPayload { + MouseEventName mouse_event_name = 1; + oneof mouse_event_payload { + uint32 line_count = 2; + action.Position position = 3; + } +} + +enum MouseEventName { + MouseScrollUp = 0; + MouseScrollDown = 1; + MouseLeftClick = 2; + MouseRightClick = 3; + MouseHold = 4; + MouseRelease = 5; +} + +message TabUpdatePayload { + repeated TabInfo tab_info = 1; +} + +message PaneUpdatePayload { + repeated PaneManifest pane_manifest = 1; +} + +message PaneManifest { + uint32 tab_index = 1; + repeated PaneInfo panes = 2; +} + +message PaneInfo { + uint32 id = 1; + bool is_plugin = 2; + bool is_focused = 3; + bool is_fullscreen = 4; + bool is_floating = 5; + bool is_suppressed = 6; + string title = 7; + bool exited = 8; + optional int32 exit_status = 9; + bool is_held = 10; + uint32 pane_x = 11; + uint32 pane_content_x = 12; + uint32 pane_y = 13; + uint32 pane_content_y = 14; + uint32 pane_rows = 15; + uint32 pane_content_rows = 16; + uint32 pane_columns = 17; + uint32 pane_content_columns = 18; + optional action.Position cursor_coordinates_in_pane = 19; + optional string terminal_command = 20; + optional string plugin_url = 21; + bool is_selectable = 22; +} + +message TabInfo { + uint32 position = 1; + string name = 2; + bool active = 3; + uint32 panes_to_hide = 4; + bool is_fullscreen_active = 5; + bool is_sync_panes_active = 6; + bool are_floating_panes_visible = 7; + repeated uint32 other_focused_clients = 8; + optional string active_swap_layout_name = 9; + bool is_swap_layout_dirty = 10; +} + +message ModeUpdatePayload { + input_mode.InputMode current_mode = 1; + repeated InputModeKeybinds keybinds = 2; + style.Style style = 3; + bool arrow_fonts_support = 4; + optional string session_name = 5; +} + +message InputModeKeybinds { + input_mode.InputMode mode = 1; + repeated KeyBind key_bind = 2; +} + +message KeyBind { + key.Key key = 1; + repeated action.Action action = 2; +} diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs new file mode 100644 index 0000000000..315fa54b81 --- /dev/null +++ b/zellij-utils/src/plugin_api/event.rs @@ -0,0 +1,1059 @@ +pub use super::generated_api::api::{ + action::{Action as ProtobufAction, Position as ProtobufPosition}, + event::{ + event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination, + Event as ProtobufEvent, EventNameList as ProtobufEventNameList, + EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds, + KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload, + PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest, + TabInfo as ProtobufTabInfo, *, + }, + input_mode::InputMode as ProtobufInputMode, + key::Key as ProtobufKey, + style::Style as ProtobufStyle, +}; +use crate::data::{ + CopyDestination, Direction, Event, EventType, InputMode, Key, ModeInfo, Mouse, Palette, + PaletteColor, PaneInfo, PaneManifest, PluginCapabilities, Style, TabInfo, ThemeHue, +}; +use crate::errors::prelude::*; +use crate::input::actions::Action; + +use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; +use std::path::PathBuf; + +impl TryFrom for Event { + type Error = &'static str; + fn try_from(protobuf_event: ProtobufEvent) -> Result { + match ProtobufEventType::from_i32(protobuf_event.name) { + Some(ProtobufEventType::ModeUpdate) => match protobuf_event.payload { + Some(ProtobufEventPayload::ModeUpdatePayload(protobuf_mode_update_payload)) => { + let mode_info: ModeInfo = protobuf_mode_update_payload.try_into()?; + Ok(Event::ModeUpdate(mode_info)) + }, + _ => Err("Malformed payload for the ModeUpdate Event"), + }, + Some(ProtobufEventType::TabUpdate) => match protobuf_event.payload { + Some(ProtobufEventPayload::TabUpdatePayload(protobuf_tab_info_payload)) => { + let mut tab_infos: Vec = vec![]; + for protobuf_tab_info in protobuf_tab_info_payload.tab_info { + tab_infos.push(TabInfo::try_from(protobuf_tab_info)?); + } + Ok(Event::TabUpdate(tab_infos)) + }, + _ => Err("Malformed payload for the TabUpdate Event"), + }, + Some(ProtobufEventType::PaneUpdate) => match protobuf_event.payload { + Some(ProtobufEventPayload::PaneUpdatePayload(protobuf_pane_update_payload)) => { + let mut pane_manifest: HashMap> = HashMap::new(); + for protobuf_pane_manifest in protobuf_pane_update_payload.pane_manifest { + let tab_index = protobuf_pane_manifest.tab_index as usize; + let mut panes = vec![]; + for protobuf_pane_info in protobuf_pane_manifest.panes { + panes.push(protobuf_pane_info.try_into()?); + } + if pane_manifest.contains_key(&tab_index) { + return Err("Duplicate tab definition in pane manifest"); + } + pane_manifest.insert(tab_index, panes); + } + Ok(Event::PaneUpdate(PaneManifest { + panes: pane_manifest, + })) + }, + _ => Err("Malformed payload for the PaneUpdate Event"), + }, + Some(ProtobufEventType::Key) => match protobuf_event.payload { + Some(ProtobufEventPayload::KeyPayload(protobuf_key)) => { + Ok(Event::Key(protobuf_key.try_into()?)) + }, + _ => Err("Malformed payload for the Key Event"), + }, + Some(ProtobufEventType::Mouse) => match protobuf_event.payload { + Some(ProtobufEventPayload::MouseEventPayload(protobuf_mouse)) => { + Ok(Event::Mouse(protobuf_mouse.try_into()?)) + }, + _ => Err("Malformed payload for the Mouse Event"), + }, + Some(ProtobufEventType::Timer) => match protobuf_event.payload { + Some(ProtobufEventPayload::TimerPayload(seconds)) => { + Ok(Event::Timer(seconds as f64)) + }, + _ => Err("Malformed payload for the Timer Event"), + }, + Some(ProtobufEventType::CopyToClipboard) => match protobuf_event.payload { + Some(ProtobufEventPayload::CopyToClipboardPayload(copy_to_clipboard)) => { + let protobuf_copy_to_clipboard = + ProtobufCopyDestination::from_i32(copy_to_clipboard) + .ok_or("Malformed copy to clipboard payload")?; + Ok(Event::CopyToClipboard( + protobuf_copy_to_clipboard.try_into()?, + )) + }, + _ => Err("Malformed payload for the Copy To Clipboard Event"), + }, + Some(ProtobufEventType::SystemClipboardFailure) => match protobuf_event.payload { + None => Ok(Event::SystemClipboardFailure), + _ => Err("Malformed payload for the system clipboard failure Event"), + }, + Some(ProtobufEventType::InputReceived) => match protobuf_event.payload { + None => Ok(Event::InputReceived), + _ => Err("Malformed payload for the input received Event"), + }, + Some(ProtobufEventType::Visible) => match protobuf_event.payload { + Some(ProtobufEventPayload::VisiblePayload(is_visible)) => { + Ok(Event::Visible(is_visible)) + }, + _ => Err("Malformed payload for the visible Event"), + }, + Some(ProtobufEventType::CustomMessage) => match protobuf_event.payload { + Some(ProtobufEventPayload::CustomMessagePayload(custom_message_payload)) => { + Ok(Event::CustomMessage( + custom_message_payload.message_name, + custom_message_payload.payload, + )) + }, + _ => Err("Malformed payload for the custom message Event"), + }, + Some(ProtobufEventType::FileSystemCreate) => match protobuf_event.payload { + Some(ProtobufEventPayload::FileListPayload(file_list_payload)) => { + let file_paths = file_list_payload + .paths + .iter() + .map(|p| PathBuf::from(p)) + .collect(); + Ok(Event::FileSystemCreate(file_paths)) + }, + _ => Err("Malformed payload for the file system create Event"), + }, + Some(ProtobufEventType::FileSystemRead) => match protobuf_event.payload { + Some(ProtobufEventPayload::FileListPayload(file_list_payload)) => { + let file_paths = file_list_payload + .paths + .iter() + .map(|p| PathBuf::from(p)) + .collect(); + Ok(Event::FileSystemRead(file_paths)) + }, + _ => Err("Malformed payload for the file system read Event"), + }, + Some(ProtobufEventType::FileSystemUpdate) => match protobuf_event.payload { + Some(ProtobufEventPayload::FileListPayload(file_list_payload)) => { + let file_paths = file_list_payload + .paths + .iter() + .map(|p| PathBuf::from(p)) + .collect(); + Ok(Event::FileSystemUpdate(file_paths)) + }, + _ => Err("Malformed payload for the file system update Event"), + }, + Some(ProtobufEventType::FileSystemDelete) => match protobuf_event.payload { + Some(ProtobufEventPayload::FileListPayload(file_list_payload)) => { + let file_paths = file_list_payload + .paths + .iter() + .map(|p| PathBuf::from(p)) + .collect(); + Ok(Event::FileSystemDelete(file_paths)) + }, + _ => Err("Malformed payload for the file system delete Event"), + }, + None => Err("Unknown Protobuf Event"), + } + } +} + +impl TryFrom for ProtobufEvent { + type Error = &'static str; + fn try_from(event: Event) -> Result { + match event { + Event::ModeUpdate(mode_info) => { + let protobuf_mode_update_payload = mode_info.try_into()?; + Ok(ProtobufEvent { + name: ProtobufEventType::ModeUpdate as i32, + payload: Some(event::Payload::ModeUpdatePayload( + protobuf_mode_update_payload, + )), + }) + }, + Event::TabUpdate(tab_infos) => { + let mut protobuf_tab_infos = vec![]; + for tab_info in tab_infos { + protobuf_tab_infos.push(tab_info.try_into()?); + } + let tab_update_payload = TabUpdatePayload { + tab_info: protobuf_tab_infos, + }; + Ok(ProtobufEvent { + name: ProtobufEventType::TabUpdate as i32, + payload: Some(event::Payload::TabUpdatePayload(tab_update_payload)), + }) + }, + Event::PaneUpdate(pane_manifest) => { + let mut protobuf_pane_manifests = vec![]; + for (tab_index, pane_infos) in pane_manifest.panes { + let mut protobuf_pane_infos = vec![]; + for pane_info in pane_infos { + protobuf_pane_infos.push(pane_info.try_into()?); + } + protobuf_pane_manifests.push(ProtobufPaneManifest { + tab_index: tab_index as u32, + panes: protobuf_pane_infos, + }); + } + Ok(ProtobufEvent { + name: ProtobufEventType::PaneUpdate as i32, + payload: Some(event::Payload::PaneUpdatePayload(PaneUpdatePayload { + pane_manifest: protobuf_pane_manifests, + })), + }) + }, + Event::Key(key) => Ok(ProtobufEvent { + name: ProtobufEventType::Key as i32, + payload: Some(event::Payload::KeyPayload(key.try_into()?)), + }), + Event::Mouse(mouse_event) => { + let protobuf_mouse_payload = mouse_event.try_into()?; + Ok(ProtobufEvent { + name: ProtobufEventType::Mouse as i32, + payload: Some(event::Payload::MouseEventPayload(protobuf_mouse_payload)), + }) + }, + Event::Timer(seconds) => Ok(ProtobufEvent { + name: ProtobufEventType::Timer as i32, + payload: Some(event::Payload::TimerPayload(seconds as f32)), + }), + Event::CopyToClipboard(clipboard_destination) => { + let protobuf_copy_destination: ProtobufCopyDestination = + clipboard_destination.try_into()?; + Ok(ProtobufEvent { + name: ProtobufEventType::CopyToClipboard as i32, + payload: Some(event::Payload::CopyToClipboardPayload( + protobuf_copy_destination as i32, + )), + }) + }, + Event::SystemClipboardFailure => Ok(ProtobufEvent { + name: ProtobufEventType::SystemClipboardFailure as i32, + payload: None, + }), + Event::InputReceived => Ok(ProtobufEvent { + name: ProtobufEventType::InputReceived as i32, + payload: None, + }), + Event::Visible(is_visible) => Ok(ProtobufEvent { + name: ProtobufEventType::Visible as i32, + payload: Some(event::Payload::VisiblePayload(is_visible)), + }), + Event::CustomMessage(message, payload) => Ok(ProtobufEvent { + name: ProtobufEventType::CustomMessage as i32, + payload: Some(event::Payload::CustomMessagePayload(CustomMessagePayload { + message_name: message, + payload, + })), + }), + Event::FileSystemCreate(paths) => { + let file_list_payload = FileListPayload { + paths: paths.iter().map(|p| p.display().to_string()).collect(), + }; + Ok(ProtobufEvent { + name: ProtobufEventType::FileSystemCreate as i32, + payload: Some(event::Payload::FileListPayload(file_list_payload)), + }) + }, + Event::FileSystemRead(paths) => { + let file_list_payload = FileListPayload { + paths: paths.iter().map(|p| p.display().to_string()).collect(), + }; + Ok(ProtobufEvent { + name: ProtobufEventType::FileSystemRead as i32, + payload: Some(event::Payload::FileListPayload(file_list_payload)), + }) + }, + Event::FileSystemUpdate(paths) => { + let file_list_payload = FileListPayload { + paths: paths.iter().map(|p| p.display().to_string()).collect(), + }; + Ok(ProtobufEvent { + name: ProtobufEventType::FileSystemUpdate as i32, + payload: Some(event::Payload::FileListPayload(file_list_payload)), + }) + }, + Event::FileSystemDelete(paths) => { + let file_list_payload = FileListPayload { + paths: paths.iter().map(|p| p.display().to_string()).collect(), + }; + Ok(ProtobufEvent { + name: ProtobufEventType::FileSystemDelete as i32, + payload: Some(event::Payload::FileListPayload(file_list_payload)), + }) + }, + } + } +} + +impl TryFrom for ProtobufCopyDestination { + type Error = &'static str; + fn try_from(copy_destination: CopyDestination) -> Result { + match copy_destination { + CopyDestination::Command => Ok(ProtobufCopyDestination::Command), + CopyDestination::Primary => Ok(ProtobufCopyDestination::Primary), + CopyDestination::System => Ok(ProtobufCopyDestination::System), + } + } +} + +impl TryFrom for CopyDestination { + type Error = &'static str; + fn try_from(protobuf_copy_destination: ProtobufCopyDestination) -> Result { + match protobuf_copy_destination { + ProtobufCopyDestination::Command => Ok(CopyDestination::Command), + ProtobufCopyDestination::Primary => Ok(CopyDestination::Primary), + ProtobufCopyDestination::System => Ok(CopyDestination::System), + } + } +} + +impl TryFrom for Mouse { + type Error = &'static str; + fn try_from(mouse_event_payload: MouseEventPayload) -> Result { + match MouseEventName::from_i32(mouse_event_payload.mouse_event_name) { + Some(MouseEventName::MouseScrollUp) => match mouse_event_payload.mouse_event_payload { + Some(mouse_event_payload::MouseEventPayload::LineCount(line_count)) => { + Ok(Mouse::ScrollUp(line_count as usize)) + }, + _ => Err("Malformed payload for mouse scroll up"), + }, + Some(MouseEventName::MouseScrollDown) => { + match mouse_event_payload.mouse_event_payload { + Some(mouse_event_payload::MouseEventPayload::LineCount(line_count)) => { + Ok(Mouse::ScrollDown(line_count as usize)) + }, + _ => Err("Malformed payload for mouse scroll down"), + } + }, + Some(MouseEventName::MouseLeftClick) => match mouse_event_payload.mouse_event_payload { + Some(mouse_event_payload::MouseEventPayload::Position(position)) => Ok( + Mouse::LeftClick(position.line as isize, position.column as usize), + ), + _ => Err("Malformed payload for mouse left click"), + }, + Some(MouseEventName::MouseRightClick) => { + match mouse_event_payload.mouse_event_payload { + Some(mouse_event_payload::MouseEventPayload::Position(position)) => Ok( + Mouse::RightClick(position.line as isize, position.column as usize), + ), + _ => Err("Malformed payload for mouse right click"), + } + }, + Some(MouseEventName::MouseHold) => match mouse_event_payload.mouse_event_payload { + Some(mouse_event_payload::MouseEventPayload::Position(position)) => Ok( + Mouse::Hold(position.line as isize, position.column as usize), + ), + _ => Err("Malformed payload for mouse hold"), + }, + Some(MouseEventName::MouseRelease) => match mouse_event_payload.mouse_event_payload { + Some(mouse_event_payload::MouseEventPayload::Position(position)) => Ok( + Mouse::Release(position.line as isize, position.column as usize), + ), + _ => Err("Malformed payload for mouse release"), + }, + None => Err("Malformed payload for MouseEventName"), + } + } +} + +impl TryFrom for MouseEventPayload { + type Error = &'static str; + fn try_from(mouse: Mouse) -> Result { + match mouse { + Mouse::ScrollUp(number_of_lines) => Ok(MouseEventPayload { + mouse_event_name: MouseEventName::MouseScrollUp as i32, + mouse_event_payload: Some(mouse_event_payload::MouseEventPayload::LineCount( + number_of_lines as u32, + )), + }), + Mouse::ScrollDown(number_of_lines) => Ok(MouseEventPayload { + mouse_event_name: MouseEventName::MouseScrollDown as i32, + mouse_event_payload: Some(mouse_event_payload::MouseEventPayload::LineCount( + number_of_lines as u32, + )), + }), + Mouse::LeftClick(line, column) => Ok(MouseEventPayload { + mouse_event_name: MouseEventName::MouseLeftClick as i32, + mouse_event_payload: Some(mouse_event_payload::MouseEventPayload::Position( + ProtobufPosition { + line: line as i64, + column: column as i64, + }, + )), + }), + Mouse::RightClick(line, column) => Ok(MouseEventPayload { + mouse_event_name: MouseEventName::MouseRightClick as i32, + mouse_event_payload: Some(mouse_event_payload::MouseEventPayload::Position( + ProtobufPosition { + line: line as i64, + column: column as i64, + }, + )), + }), + Mouse::Hold(line, column) => Ok(MouseEventPayload { + mouse_event_name: MouseEventName::MouseHold as i32, + mouse_event_payload: Some(mouse_event_payload::MouseEventPayload::Position( + ProtobufPosition { + line: line as i64, + column: column as i64, + }, + )), + }), + Mouse::Release(line, column) => Ok(MouseEventPayload { + mouse_event_name: MouseEventName::MouseRelease as i32, + mouse_event_payload: Some(mouse_event_payload::MouseEventPayload::Position( + ProtobufPosition { + line: line as i64, + column: column as i64, + }, + )), + }), + } + } +} + +impl TryFrom for PaneInfo { + type Error = &'static str; + fn try_from(protobuf_pane_info: ProtobufPaneInfo) -> Result { + Ok(PaneInfo { + id: protobuf_pane_info.id, + is_plugin: protobuf_pane_info.is_plugin, + is_focused: protobuf_pane_info.is_focused, + is_fullscreen: protobuf_pane_info.is_fullscreen, + is_floating: protobuf_pane_info.is_floating, + is_suppressed: protobuf_pane_info.is_suppressed, + title: protobuf_pane_info.title, + exited: protobuf_pane_info.exited, + exit_status: protobuf_pane_info.exit_status, + is_held: protobuf_pane_info.is_held, + pane_x: protobuf_pane_info.pane_x as usize, + pane_content_x: protobuf_pane_info.pane_content_x as usize, + pane_y: protobuf_pane_info.pane_y as usize, + pane_content_y: protobuf_pane_info.pane_content_y as usize, + pane_rows: protobuf_pane_info.pane_rows as usize, + pane_content_rows: protobuf_pane_info.pane_content_rows as usize, + pane_columns: protobuf_pane_info.pane_columns as usize, + pane_content_columns: protobuf_pane_info.pane_content_columns as usize, + cursor_coordinates_in_pane: protobuf_pane_info + .cursor_coordinates_in_pane + .map(|position| (position.column as usize, position.line as usize)), + terminal_command: protobuf_pane_info.terminal_command, + plugin_url: protobuf_pane_info.plugin_url, + is_selectable: protobuf_pane_info.is_selectable, + }) + } +} + +impl TryFrom for ProtobufPaneInfo { + type Error = &'static str; + fn try_from(pane_info: PaneInfo) -> Result { + Ok(ProtobufPaneInfo { + id: pane_info.id, + is_plugin: pane_info.is_plugin, + is_focused: pane_info.is_focused, + is_fullscreen: pane_info.is_fullscreen, + is_floating: pane_info.is_floating, + is_suppressed: pane_info.is_suppressed, + title: pane_info.title, + exited: pane_info.exited, + exit_status: pane_info.exit_status, + is_held: pane_info.is_held, + pane_x: pane_info.pane_x as u32, + pane_content_x: pane_info.pane_content_x as u32, + pane_y: pane_info.pane_y as u32, + pane_content_y: pane_info.pane_content_y as u32, + pane_rows: pane_info.pane_rows as u32, + pane_content_rows: pane_info.pane_content_rows as u32, + pane_columns: pane_info.pane_columns as u32, + pane_content_columns: pane_info.pane_content_columns as u32, + cursor_coordinates_in_pane: pane_info.cursor_coordinates_in_pane.map(|(x, y)| { + ProtobufPosition { + column: x as i64, + line: y as i64, + } + }), + terminal_command: pane_info.terminal_command, + plugin_url: pane_info.plugin_url, + is_selectable: pane_info.is_selectable, + }) + } +} + +impl TryFrom for TabInfo { + type Error = &'static str; + fn try_from(protobuf_tab_info: ProtobufTabInfo) -> Result { + Ok(TabInfo { + position: protobuf_tab_info.position as usize, + name: protobuf_tab_info.name, + active: protobuf_tab_info.active, + panes_to_hide: protobuf_tab_info.panes_to_hide as usize, + is_fullscreen_active: protobuf_tab_info.is_fullscreen_active, + is_sync_panes_active: protobuf_tab_info.is_sync_panes_active, + are_floating_panes_visible: protobuf_tab_info.are_floating_panes_visible, + other_focused_clients: protobuf_tab_info + .other_focused_clients + .iter() + .map(|c| *c as u16) + .collect(), + active_swap_layout_name: protobuf_tab_info.active_swap_layout_name, + is_swap_layout_dirty: protobuf_tab_info.is_swap_layout_dirty, + }) + } +} + +impl TryFrom for ProtobufTabInfo { + type Error = &'static str; + fn try_from(tab_info: TabInfo) -> Result { + Ok(ProtobufTabInfo { + position: tab_info.position as u32, + name: tab_info.name, + active: tab_info.active, + panes_to_hide: tab_info.panes_to_hide as u32, + is_fullscreen_active: tab_info.is_fullscreen_active, + is_sync_panes_active: tab_info.is_sync_panes_active, + are_floating_panes_visible: tab_info.are_floating_panes_visible, + other_focused_clients: tab_info + .other_focused_clients + .iter() + .map(|c| *c as u32) + .collect(), + active_swap_layout_name: tab_info.active_swap_layout_name, + is_swap_layout_dirty: tab_info.is_swap_layout_dirty, + }) + } +} + +impl TryFrom for ModeInfo { + type Error = &'static str; + fn try_from( + mut protobuf_mode_update_payload: ProtobufModeUpdatePayload, + ) -> Result { + let current_mode: InputMode = + ProtobufInputMode::from_i32(protobuf_mode_update_payload.current_mode) + .ok_or("Malformed InputMode in the ModeUpdate Event")? + .try_into()?; + let keybinds: Vec<(InputMode, Vec<(Key, Vec)>)> = protobuf_mode_update_payload + .keybinds + .iter_mut() + .filter_map(|k| { + let input_mode: InputMode = ProtobufInputMode::from_i32(k.mode) + .ok_or("Malformed InputMode in the ModeUpdate Event") + .ok()? + .try_into() + .ok()?; + let mut keybinds: Vec<(Key, Vec)> = vec![]; + for mut protobuf_keybind in k.key_bind.drain(..) { + let key: Key = protobuf_keybind.key.unwrap().try_into().ok()?; + let mut actions: Vec = vec![]; + for action in protobuf_keybind.action.drain(..) { + if let Ok(action) = action.try_into() { + actions.push(action); + } + } + keybinds.push((key, actions)); + } + Some((input_mode, keybinds)) + }) + .collect(); + let style: Style = protobuf_mode_update_payload + .style + .and_then(|m| m.try_into().ok()) + .ok_or("malformed payload for mode_info")?; + let session_name = protobuf_mode_update_payload.session_name; + let capabilities = PluginCapabilities { + arrow_fonts: protobuf_mode_update_payload.arrow_fonts_support, + }; + let mode_info = ModeInfo { + mode: current_mode, + keybinds, + style, + capabilities, + session_name, + }; + Ok(mode_info) + } +} + +impl TryFrom for ProtobufModeUpdatePayload { + type Error = &'static str; + fn try_from(mode_info: ModeInfo) -> Result { + let current_mode: ProtobufInputMode = mode_info.mode.try_into()?; + let style: ProtobufStyle = mode_info.style.try_into()?; + let arrow_fonts_support: bool = mode_info.capabilities.arrow_fonts; + let session_name = mode_info.session_name; + let mut protobuf_input_mode_keybinds: Vec = vec![]; + for (input_mode, input_mode_keybinds) in mode_info.keybinds { + let mode: ProtobufInputMode = input_mode.try_into()?; + let mut keybinds: Vec = vec![]; + for (key, actions) in input_mode_keybinds { + let protobuf_key: ProtobufKey = key.try_into()?; + let mut protobuf_actions: Vec = vec![]; + for action in actions { + if let Ok(protobuf_action) = action.try_into() { + protobuf_actions.push(protobuf_action); + } + } + let key_bind = ProtobufKeyBind { + key: Some(protobuf_key), + action: protobuf_actions, + }; + keybinds.push(key_bind); + } + let input_mode_keybind = ProtobufInputModeKeybinds { + mode: mode as i32, + key_bind: keybinds, + }; + protobuf_input_mode_keybinds.push(input_mode_keybind); + } + Ok(ProtobufModeUpdatePayload { + current_mode: current_mode as i32, + style: Some(style), + keybinds: protobuf_input_mode_keybinds, + arrow_fonts_support, + session_name, + }) + } +} + +impl TryFrom for HashSet { + type Error = &'static str; + fn try_from(protobuf_event_name_list: ProtobufEventNameList) -> Result { + let event_types: Vec = protobuf_event_name_list + .event_types + .iter() + .filter_map(|i| ProtobufEventType::from_i32(*i)) + .collect(); + let event_types: Vec = event_types + .iter() + .filter_map(|e| EventType::try_from(*e).ok()) + .collect(); + Ok(event_types.into_iter().collect()) + } +} + +impl TryFrom> for ProtobufEventNameList { + type Error = &'static str; + fn try_from(event_types: HashSet) -> Result { + let protobuf_event_name_list = ProtobufEventNameList { + event_types: event_types + .iter() + .filter_map(|e| ProtobufEventType::try_from(*e).ok()) + .map(|e| e as i32) + .collect(), + }; + Ok(protobuf_event_name_list) + } +} + +impl TryFrom for EventType { + type Error = &'static str; + fn try_from(protobuf_event_type: ProtobufEventType) -> Result { + Ok(match protobuf_event_type { + ProtobufEventType::ModeUpdate => EventType::ModeUpdate, + ProtobufEventType::TabUpdate => EventType::TabUpdate, + ProtobufEventType::PaneUpdate => EventType::PaneUpdate, + ProtobufEventType::Key => EventType::Key, + ProtobufEventType::Mouse => EventType::Mouse, + ProtobufEventType::Timer => EventType::Timer, + ProtobufEventType::CopyToClipboard => EventType::CopyToClipboard, + ProtobufEventType::SystemClipboardFailure => EventType::SystemClipboardFailure, + ProtobufEventType::InputReceived => EventType::InputReceived, + ProtobufEventType::Visible => EventType::Visible, + ProtobufEventType::CustomMessage => EventType::CustomMessage, + ProtobufEventType::FileSystemCreate => EventType::FileSystemCreate, + ProtobufEventType::FileSystemRead => EventType::FileSystemRead, + ProtobufEventType::FileSystemUpdate => EventType::FileSystemUpdate, + ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete, + }) + } +} + +impl TryFrom for ProtobufEventType { + type Error = &'static str; + fn try_from(event_type: EventType) -> Result { + Ok(match event_type { + EventType::ModeUpdate => ProtobufEventType::ModeUpdate, + EventType::TabUpdate => ProtobufEventType::TabUpdate, + EventType::PaneUpdate => ProtobufEventType::PaneUpdate, + EventType::Key => ProtobufEventType::Key, + EventType::Mouse => ProtobufEventType::Mouse, + EventType::Timer => ProtobufEventType::Timer, + EventType::CopyToClipboard => ProtobufEventType::CopyToClipboard, + EventType::SystemClipboardFailure => ProtobufEventType::SystemClipboardFailure, + EventType::InputReceived => ProtobufEventType::InputReceived, + EventType::Visible => ProtobufEventType::Visible, + EventType::CustomMessage => ProtobufEventType::CustomMessage, + EventType::FileSystemCreate => ProtobufEventType::FileSystemCreate, + EventType::FileSystemRead => ProtobufEventType::FileSystemRead, + EventType::FileSystemUpdate => ProtobufEventType::FileSystemUpdate, + EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete, + }) + } +} + +#[test] +fn serialize_mode_update_event() { + use prost::Message; + let mode_update_event = Event::ModeUpdate(Default::default()); + let protobuf_event: ProtobufEvent = mode_update_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + mode_update_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_mode_update_event_with_non_default_values() { + use prost::Message; + let mode_update_event = Event::ModeUpdate(ModeInfo { + mode: InputMode::Locked, + keybinds: vec![ + ( + InputMode::Locked, + vec![( + Key::Alt(crate::data::CharOrArrow::Char('b')), + vec![Action::SwitchToMode(InputMode::Normal)], + )], + ), + ( + InputMode::Tab, + vec![( + Key::Alt(crate::data::CharOrArrow::Direction(Direction::Up)), + vec![Action::SwitchToMode(InputMode::Pane)], + )], + ), + ( + InputMode::Pane, + vec![ + ( + Key::Ctrl('b'), + vec![ + Action::SwitchToMode(InputMode::Tmux), + Action::Write(vec![10]), + ], + ), + (Key::Char('a'), vec![Action::WriteChars("foo".to_owned())]), + ], + ), + ], + style: Style { + colors: Palette { + source: crate::data::PaletteSource::Default, + theme_hue: ThemeHue::Light, + fg: PaletteColor::Rgb((1, 1, 1)), + bg: PaletteColor::Rgb((200, 200, 200)), + black: PaletteColor::EightBit(1), + red: PaletteColor::EightBit(2), + green: PaletteColor::EightBit(2), + yellow: PaletteColor::EightBit(2), + blue: PaletteColor::EightBit(2), + magenta: PaletteColor::EightBit(2), + cyan: PaletteColor::EightBit(2), + white: PaletteColor::EightBit(2), + orange: PaletteColor::EightBit(2), + gray: PaletteColor::EightBit(2), + purple: PaletteColor::EightBit(2), + gold: PaletteColor::EightBit(2), + silver: PaletteColor::EightBit(2), + pink: PaletteColor::EightBit(2), + brown: PaletteColor::Rgb((222, 221, 220)), + }, + rounded_corners: true, + hide_session_name: false, + }, + capabilities: PluginCapabilities { arrow_fonts: false }, + session_name: Some("my awesome test session".to_owned()), + }); + let protobuf_event: ProtobufEvent = mode_update_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + mode_update_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_tab_update_event() { + use prost::Message; + let tab_update_event = Event::TabUpdate(Default::default()); + let protobuf_event: ProtobufEvent = tab_update_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + tab_update_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_tab_update_event_with_non_default_values() { + use prost::Message; + let tab_update_event = Event::TabUpdate(vec![ + TabInfo { + position: 0, + name: "First tab".to_owned(), + active: true, + panes_to_hide: 2, + is_fullscreen_active: true, + is_sync_panes_active: false, + are_floating_panes_visible: true, + other_focused_clients: vec![2, 3, 4], + active_swap_layout_name: Some("my cool swap layout".to_owned()), + is_swap_layout_dirty: false, + }, + TabInfo { + position: 1, + name: "Secondtab".to_owned(), + active: false, + panes_to_hide: 5, + is_fullscreen_active: false, + is_sync_panes_active: true, + are_floating_panes_visible: true, + other_focused_clients: vec![1, 5, 111], + active_swap_layout_name: None, + is_swap_layout_dirty: true, + }, + TabInfo::default(), + ]); + let protobuf_event: ProtobufEvent = tab_update_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + tab_update_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_pane_update_event() { + use prost::Message; + let pane_update_event = Event::PaneUpdate(Default::default()); + let protobuf_event: ProtobufEvent = pane_update_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + pane_update_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_key_event() { + use prost::Message; + let key_event = Event::Key(Key::Ctrl('a')); + let protobuf_event: ProtobufEvent = key_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + key_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_mouse_event() { + use prost::Message; + let mouse_event = Event::Mouse(Mouse::LeftClick(1, 1)); + let protobuf_event: ProtobufEvent = mouse_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + mouse_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_mouse_event_without_position() { + use prost::Message; + let mouse_event = Event::Mouse(Mouse::ScrollUp(17)); + let protobuf_event: ProtobufEvent = mouse_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + mouse_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_timer_event() { + use prost::Message; + let timer_event = Event::Timer(1.5); + let protobuf_event: ProtobufEvent = timer_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + timer_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_copy_to_clipboard_event() { + use prost::Message; + let copy_event = Event::CopyToClipboard(CopyDestination::Primary); + let protobuf_event: ProtobufEvent = copy_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + copy_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_clipboard_failure_event() { + use prost::Message; + let copy_event = Event::SystemClipboardFailure; + let protobuf_event: ProtobufEvent = copy_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + copy_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_input_received_event() { + use prost::Message; + let input_received_event = Event::InputReceived; + let protobuf_event: ProtobufEvent = input_received_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + input_received_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_visible_event() { + use prost::Message; + let visible_event = Event::Visible(true); + let protobuf_event: ProtobufEvent = visible_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + visible_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_custom_message_event() { + use prost::Message; + let custom_message_event = Event::CustomMessage("foo".to_owned(), "bar".to_owned()); + let protobuf_event: ProtobufEvent = custom_message_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + custom_message_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_file_system_create_event() { + use prost::Message; + let file_system_event = + Event::FileSystemCreate(vec!["/absolute/path".into(), "./relative_path".into()]); + let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + file_system_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_file_system_read_event() { + use prost::Message; + let file_system_event = + Event::FileSystemRead(vec!["/absolute/path".into(), "./relative_path".into()]); + let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + file_system_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_file_system_update_event() { + use prost::Message; + let file_system_event = + Event::FileSystemUpdate(vec!["/absolute/path".into(), "./relative_path".into()]); + let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + file_system_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_file_system_delete_event() { + use prost::Message; + let file_system_event = + Event::FileSystemDelete(vec!["/absolute/path".into(), "./relative_path".into()]); + let protobuf_event: ProtobufEvent = file_system_event.clone().try_into().unwrap(); + let serialized_protobuf_event = protobuf_event.encode_to_vec(); + let deserialized_protobuf_event: ProtobufEvent = + Message::decode(serialized_protobuf_event.as_slice()).unwrap(); + let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap(); + assert_eq!( + file_system_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} diff --git a/zellij-utils/src/plugin_api/file.proto b/zellij-utils/src/plugin_api/file.proto new file mode 100644 index 0000000000..b2c5372b83 --- /dev/null +++ b/zellij-utils/src/plugin_api/file.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package api.file; + +message File { + string path = 1; + optional int32 line_number = 2; + optional string cwd = 3; +} diff --git a/zellij-utils/src/plugin_api/file.rs b/zellij-utils/src/plugin_api/file.rs new file mode 100644 index 0000000000..7c06aa8e04 --- /dev/null +++ b/zellij-utils/src/plugin_api/file.rs @@ -0,0 +1,30 @@ +pub use super::generated_api::api::file::File as ProtobufFile; +use crate::data::FileToOpen; + +use std::convert::TryFrom; +use std::path::PathBuf; + +impl TryFrom for FileToOpen { + type Error = &'static str; + fn try_from(protobuf_file: ProtobufFile) -> Result { + let path = PathBuf::from(protobuf_file.path); + let line_number = protobuf_file.line_number.map(|l| l as usize); + let cwd = protobuf_file.cwd.map(|c| PathBuf::from(c)); + Ok(FileToOpen { + path, + line_number, + cwd, + }) + } +} + +impl TryFrom for ProtobufFile { + type Error = &'static str; + fn try_from(file_to_open: FileToOpen) -> Result { + Ok(ProtobufFile { + path: file_to_open.path.display().to_string(), + line_number: file_to_open.line_number.map(|l| l as i32), + cwd: file_to_open.cwd.map(|c| c.display().to_string()), + }) + } +} diff --git a/zellij-utils/src/plugin_api/input_mode.proto b/zellij-utils/src/plugin_api/input_mode.proto new file mode 100644 index 0000000000..3112249527 --- /dev/null +++ b/zellij-utils/src/plugin_api/input_mode.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package api.input_mode; + +message InputModeMessage { + InputMode input_mode = 1; +} + +enum InputMode { + /// In `Normal` mode, input is always written to the terminal, except for the shortcuts leading + /// to other modes + Normal = 0; + /// In `Locked` mode, input is always written to the terminal and all shortcuts are disabled + /// except the one leading back to normal mode + Locked = 1; + /// `Resize` mode allows resizing the different existing panes. + Resize = 2; + /// `Pane` mode allows creating and closing panes, as well as moving between them. + Pane = 3; + /// `Tab` mode allows creating and closing tabs, as well as moving between them. + Tab = 4; + /// `Scroll` mode allows scrolling up and down within a pane. + Scroll = 5; + /// `EnterSearch` mode allows for typing in the needle for a search in the scroll buffer of a pane. + EnterSearch = 6; + /// `Search` mode allows for searching a term in a pane (superset of `Scroll`). + Search = 7; + /// `RenameTab` mode allows assigning a new name to a tab. + RenameTab = 8; + /// `RenamePane` mode allows assigning a new name to a pane. + RenamePane = 9; + /// `Session` mode allows detaching sessions + Session = 10; + /// `Move` mode allows moving the different existing panes within a tab + Move = 11; + /// `Prompt` mode allows interacting with active prompts. + Prompt = 12; + /// `Tmux` mode allows for basic tmux keybindings functionality + Tmux = 13; +} diff --git a/zellij-utils/src/plugin_api/input_mode.rs b/zellij-utils/src/plugin_api/input_mode.rs new file mode 100644 index 0000000000..83a6a00970 --- /dev/null +++ b/zellij-utils/src/plugin_api/input_mode.rs @@ -0,0 +1,69 @@ +pub use super::generated_api::api::input_mode::{ + InputMode as ProtobufInputMode, InputModeMessage as ProtobufInputModeMessage, +}; +use crate::data::InputMode; + +use std::convert::TryFrom; + +impl TryFrom for InputMode { + type Error = &'static str; + fn try_from(protobuf_input_mode: ProtobufInputMode) -> Result { + match protobuf_input_mode { + ProtobufInputMode::Normal => Ok(InputMode::Normal), + ProtobufInputMode::Locked => Ok(InputMode::Locked), + ProtobufInputMode::Resize => Ok(InputMode::Resize), + ProtobufInputMode::Pane => Ok(InputMode::Pane), + ProtobufInputMode::Tab => Ok(InputMode::Tab), + ProtobufInputMode::Scroll => Ok(InputMode::Scroll), + ProtobufInputMode::EnterSearch => Ok(InputMode::EnterSearch), + ProtobufInputMode::Search => Ok(InputMode::Search), + ProtobufInputMode::RenameTab => Ok(InputMode::RenameTab), + ProtobufInputMode::RenamePane => Ok(InputMode::RenamePane), + ProtobufInputMode::Session => Ok(InputMode::Session), + ProtobufInputMode::Move => Ok(InputMode::Move), + ProtobufInputMode::Prompt => Ok(InputMode::Prompt), + ProtobufInputMode::Tmux => Ok(InputMode::Tmux), + } + } +} + +impl TryFrom for ProtobufInputMode { + type Error = &'static str; + fn try_from(input_mode: InputMode) -> Result { + Ok(match input_mode { + InputMode::Normal => ProtobufInputMode::Normal, + InputMode::Locked => ProtobufInputMode::Locked, + InputMode::Resize => ProtobufInputMode::Resize, + InputMode::Pane => ProtobufInputMode::Pane, + InputMode::Tab => ProtobufInputMode::Tab, + InputMode::Scroll => ProtobufInputMode::Scroll, + InputMode::EnterSearch => ProtobufInputMode::EnterSearch, + InputMode::Search => ProtobufInputMode::Search, + InputMode::RenameTab => ProtobufInputMode::RenameTab, + InputMode::RenamePane => ProtobufInputMode::RenamePane, + InputMode::Session => ProtobufInputMode::Session, + InputMode::Move => ProtobufInputMode::Move, + InputMode::Prompt => ProtobufInputMode::Prompt, + InputMode::Tmux => ProtobufInputMode::Tmux, + }) + } +} + +impl TryFrom for InputMode { + type Error = &'static str; + fn try_from(protobuf_input_mode: ProtobufInputModeMessage) -> Result { + ProtobufInputMode::from_i32(protobuf_input_mode.input_mode) + .and_then(|p| p.try_into().ok()) + .ok_or("Invalid input mode") + } +} + +impl TryFrom for ProtobufInputModeMessage { + type Error = &'static str; + fn try_from(input_mode: InputMode) -> Result { + let protobuf_input_mode: ProtobufInputMode = input_mode.try_into()?; + Ok(ProtobufInputModeMessage { + input_mode: protobuf_input_mode as i32, + }) + } +} diff --git a/zellij-utils/src/plugin_api/key.proto b/zellij-utils/src/plugin_api/key.proto new file mode 100644 index 0000000000..9a573483e9 --- /dev/null +++ b/zellij-utils/src/plugin_api/key.proto @@ -0,0 +1,83 @@ +syntax = "proto3"; + +package api.key; + +message Key { + enum KeyModifier { + CTRL = 0; + ALT = 1; + } + + enum NamedKey { + PageDown = 0; + PageUp = 1; + LeftArrow = 2; + DownArrow = 3; + UpArrow = 4; + RightArrow = 5; + Home = 6; + End = 7; + Backspace = 8; + Delete = 9; + Insert = 10; + F1 = 11; + F2 = 12; + F3 = 13; + F4 = 14; + F5 = 15; + F6 = 16; + F7 = 17; + F8 = 18; + F9 = 19; + F10 = 20; + F11 = 21; + F12 = 22; + Tab = 23; + Esc = 24; + } + + enum Char { + a = 0; + b = 1; + c = 2; + d = 3; + e = 4; + f = 5; + g = 6; + h = 7; + i = 8; + j = 9; + k = 10; + l = 11; + m = 12; + n = 13; + o = 14; + p = 15; + q = 16; + r = 17; + s = 18; + t = 19; + u = 20; + v = 21; + w = 22; + x = 23; + y = 24; + z = 25; + zero = 26; + one = 27; + two = 28; + three = 29; + four = 30; + five = 31; + six = 32; + seven = 33; + eight = 34; + nine = 35; + } + + optional KeyModifier modifier = 1; + oneof main_key { + NamedKey key = 2; + Char char = 3; + } +} diff --git a/zellij-utils/src/plugin_api/key.rs b/zellij-utils/src/plugin_api/key.rs new file mode 100644 index 0000000000..7cef4ba761 --- /dev/null +++ b/zellij-utils/src/plugin_api/key.rs @@ -0,0 +1,222 @@ +pub use super::generated_api::api::key::{ + key::{KeyModifier, MainKey, NamedKey}, + Key as ProtobufKey, +}; +use crate::data::{CharOrArrow, Direction, Key}; + +use std::convert::TryFrom; + +impl TryFrom for Key { + type Error = &'static str; + fn try_from(protobuf_key: ProtobufKey) -> Result { + let key_modifier = parse_optional_modifier(&protobuf_key); + match key_modifier { + Some(KeyModifier::Ctrl) => { + let character = char_from_main_key(protobuf_key.main_key)?; + Ok(Key::Ctrl(character)) + }, + Some(KeyModifier::Alt) => { + let char_or_arrow = CharOrArrow::from_main_key(protobuf_key.main_key)?; + Ok(Key::Alt(char_or_arrow)) + }, + None => match protobuf_key.main_key.as_ref().ok_or("invalid key")? { + MainKey::Char(_key_index) => { + let character = char_from_main_key(protobuf_key.main_key)?; + Ok(Key::Char(character)) + }, + MainKey::Key(key_index) => { + let key = NamedKey::from_i32(*key_index).ok_or("invalid_key")?; + Ok(named_key_to_key(key)) + }, + }, + } + } +} + +impl TryFrom for ProtobufKey { + type Error = &'static str; + fn try_from(key: Key) -> Result { + match key { + Key::PageDown => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::PageDown as i32)), + }), + Key::PageUp => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::PageUp as i32)), + }), + Key::Left => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::LeftArrow as i32)), + }), + Key::Down => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::DownArrow as i32)), + }), + Key::Up => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::UpArrow as i32)), + }), + Key::Right => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::RightArrow as i32)), + }), + Key::Home => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::Home as i32)), + }), + Key::End => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::End as i32)), + }), + Key::Backspace => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::Backspace as i32)), + }), + Key::Delete => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::Delete as i32)), + }), + Key::Insert => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::Insert as i32)), + }), + Key::F(index) => { + let main_key = match index { + 1 => Some(MainKey::Key(NamedKey::F1 as i32)), + 2 => Some(MainKey::Key(NamedKey::F2 as i32)), + 3 => Some(MainKey::Key(NamedKey::F3 as i32)), + 4 => Some(MainKey::Key(NamedKey::F4 as i32)), + 5 => Some(MainKey::Key(NamedKey::F5 as i32)), + 6 => Some(MainKey::Key(NamedKey::F6 as i32)), + 7 => Some(MainKey::Key(NamedKey::F7 as i32)), + 8 => Some(MainKey::Key(NamedKey::F8 as i32)), + 9 => Some(MainKey::Key(NamedKey::F9 as i32)), + 10 => Some(MainKey::Key(NamedKey::F10 as i32)), + 11 => Some(MainKey::Key(NamedKey::F11 as i32)), + 12 => Some(MainKey::Key(NamedKey::F12 as i32)), + _ => return Err("Invalid key"), + }; + Ok(ProtobufKey { + modifier: None, + main_key, + }) + }, + Key::Char(character) => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Char((character as u8) as i32)), + }), + Key::Alt(char_or_arrow) => { + let main_key = match char_or_arrow { + CharOrArrow::Char(character) => MainKey::Char((character as u8) as i32), + CharOrArrow::Direction(Direction::Left) => { + MainKey::Key(NamedKey::LeftArrow as i32) + }, + CharOrArrow::Direction(Direction::Right) => { + MainKey::Key(NamedKey::RightArrow as i32) + }, + CharOrArrow::Direction(Direction::Up) => MainKey::Key(NamedKey::UpArrow as i32), + CharOrArrow::Direction(Direction::Down) => { + MainKey::Key(NamedKey::DownArrow as i32) + }, + }; + Ok(ProtobufKey { + modifier: Some(KeyModifier::Alt as i32), + main_key: Some(main_key), + }) + }, + Key::Ctrl(character) => Ok(ProtobufKey { + modifier: Some(KeyModifier::Ctrl as i32), + main_key: Some(MainKey::Char((character as u8) as i32)), + }), + Key::BackTab => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::Tab as i32)), + }), + Key::Null => { + Ok(ProtobufKey { + modifier: None, + main_key: None, // TODO: does this break deserialization? + }) + }, + Key::Esc => Ok(ProtobufKey { + modifier: None, + main_key: Some(MainKey::Key(NamedKey::Esc as i32)), + }), + } + } +} + +impl CharOrArrow { + pub fn from_main_key( + main_key: std::option::Option, + ) -> Result { + match main_key { + Some(MainKey::Char(encoded_key)) => { + Ok(CharOrArrow::Char(char_index_to_char(encoded_key))) + }, + Some(MainKey::Key(key_index)) => match NamedKey::from_i32(key_index) { + Some(NamedKey::LeftArrow) => Ok(CharOrArrow::Direction(Direction::Left)), + Some(NamedKey::RightArrow) => Ok(CharOrArrow::Direction(Direction::Right)), + Some(NamedKey::UpArrow) => Ok(CharOrArrow::Direction(Direction::Up)), + Some(NamedKey::DownArrow) => Ok(CharOrArrow::Direction(Direction::Down)), + _ => Err("Unsupported key"), + }, + _ => { + return Err("Unsupported key"); + }, + } + } +} + +fn parse_optional_modifier(m: &ProtobufKey) -> Option { + match m.modifier { + Some(modifier) => KeyModifier::from_i32(modifier), + _ => None, + } +} + +fn char_index_to_char(char_index: i32) -> char { + char_index as u8 as char +} + +fn char_from_main_key(main_key: Option) -> Result { + match main_key { + Some(MainKey::Char(encoded_key)) => { + return Ok(char_index_to_char(encoded_key)); + }, + _ => { + return Err("Unsupported key"); + }, + } +} + +fn named_key_to_key(named_key: NamedKey) -> Key { + match named_key { + NamedKey::PageDown => Key::PageDown, + NamedKey::PageUp => Key::PageUp, + NamedKey::LeftArrow => Key::Left, + NamedKey::DownArrow => Key::Down, + NamedKey::UpArrow => Key::Up, + NamedKey::RightArrow => Key::Right, + NamedKey::Home => Key::Home, + NamedKey::End => Key::End, + NamedKey::Backspace => Key::Backspace, + NamedKey::Delete => Key::Delete, + NamedKey::Insert => Key::Insert, + NamedKey::F1 => Key::F(1), + NamedKey::F2 => Key::F(2), + NamedKey::F3 => Key::F(3), + NamedKey::F4 => Key::F(4), + NamedKey::F5 => Key::F(5), + NamedKey::F6 => Key::F(6), + NamedKey::F7 => Key::F(7), + NamedKey::F8 => Key::F(8), + NamedKey::F9 => Key::F(9), + NamedKey::F10 => Key::F(10), + NamedKey::F11 => Key::F(11), + NamedKey::F12 => Key::F(12), + NamedKey::Tab => Key::BackTab, + NamedKey::Esc => Key::Esc, + } +} diff --git a/zellij-utils/src/plugin_api/message.proto b/zellij-utils/src/plugin_api/message.proto new file mode 100644 index 0000000000..35d111506f --- /dev/null +++ b/zellij-utils/src/plugin_api/message.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package api.message; + +message Message { + string name = 1; + string payload = 2; + optional string worker_name = 3; +} diff --git a/zellij-utils/src/plugin_api/message.rs b/zellij-utils/src/plugin_api/message.rs new file mode 100644 index 0000000000..365a427a40 --- /dev/null +++ b/zellij-utils/src/plugin_api/message.rs @@ -0,0 +1,29 @@ +pub use super::generated_api::api::message::Message as ProtobufMessage; +use crate::data::PluginMessage; + +use std::convert::TryFrom; + +impl TryFrom for PluginMessage { + type Error = &'static str; + fn try_from(protobuf_message: ProtobufMessage) -> Result { + let name = protobuf_message.name; + let payload = protobuf_message.payload; + let worker_name = protobuf_message.worker_name; + Ok(PluginMessage { + name, + payload, + worker_name, + }) + } +} + +impl TryFrom for ProtobufMessage { + type Error = &'static str; + fn try_from(plugin_message: PluginMessage) -> Result { + Ok(ProtobufMessage { + name: plugin_message.name, + payload: plugin_message.payload, + worker_name: plugin_message.worker_name, + }) + } +} diff --git a/zellij-utils/src/plugin_api/mod.rs b/zellij-utils/src/plugin_api/mod.rs new file mode 100644 index 0000000000..55ace5007b --- /dev/null +++ b/zellij-utils/src/plugin_api/mod.rs @@ -0,0 +1,14 @@ +pub mod action; +pub mod command; +pub mod event; +pub mod file; +pub mod input_mode; +pub mod key; +pub mod message; +pub mod plugin_command; +pub mod plugin_ids; +pub mod resize; +pub mod style; +pub mod generated_api { + include!(concat!(env!("OUT_DIR"), "/generated_plugin_api.rs")); +} diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto new file mode 100644 index 0000000000..f09ccf4b14 --- /dev/null +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -0,0 +1,175 @@ +syntax = "proto3"; + +import "event.proto"; +import "file.proto"; +import "command.proto"; +import "message.proto"; +import "input_mode.proto"; +import "resize.proto"; + +package api.plugin_command; + +enum CommandName { + Subscribe = 0; + Unsubscribe = 1; + SetSelectable = 2; + GetPluginIds = 3; + GetZellijVersion = 4; + OpenFile = 5; + OpenFileFloating = 6; + OpenTerminal = 7; + OpenTerminalFloating = 8; + OpenCommandPane = 9; + OpenCommandPaneFloating = 10; + SwitchTabTo = 11; + SetTimeout = 12; + ExecCmd = 13; + PostMessageTo = 14; + PostMessageToPlugin = 15; + HideSelf = 16; + ShowSelf = 17; + SwitchToMode = 18; + NewTabsWithLayout = 19; + NewTab = 20; + GoToNextTab = 21; + GoToPreviousTab = 22; + Resize = 23; + ResizeWithDirection = 24; + FocusNextPane = 25; + FocusPreviousPane = 26; + MoveFocus = 27; + MoveFocusOrTab = 28; + Detach = 29; + EditScrollback = 30; + Write = 31; + WriteChars = 32; + ToggleTab = 33; + MovePane = 34; + MovePaneWithDirection = 35; + ClearScreen = 36; + ScrollUp = 37; + ScrollDown = 38; + ScrollToTop = 39; + ScrollToBottom = 40; + PageScrollUp = 41; + PageScrollDown = 42; + ToggleFocusFullscreen = 43; + TogglePaneFrames = 44; + TogglePaneEmbedOrEject = 45; + UndoRenamePane = 46; + CloseFocus = 47; + ToggleActiveTabSync = 48; + CloseFocusedTab = 49; + UndoRenameTab = 50; + QuitZellij = 51; + PreviousSwapLayout = 52; + NextSwapLayout = 53; + GoToTabName = 54; + FocusOrCreateTab = 55; + GoToTab = 56; + StartOrReloadPlugin = 57; + CloseTerminalPane = 58; + ClosePluginPane = 59; + FocusTerminalPane = 60; + FocusPluginPane = 61; + RenameTerminalPane = 62; + RenamePluginPane = 63; + RenameTab = 64; + ReportCrash = 65; +} + +message PluginCommand { + CommandName name = 1; + oneof payload { + SubscribePayload subscribe_payload = 2; + UnsubscribePayload unsubscribe_payload = 3; + bool set_selectable_payload = 4; + OpenFilePayload open_file_payload = 5; + OpenFilePayload open_file_floating_payload = 6; + OpenFilePayload open_terminal_payload = 7; + OpenFilePayload open_terminal_floating_payload = 8; + OpenCommandPanePayload open_command_pane_payload = 9; + OpenCommandPanePayload open_command_pane_floating_payload = 10; + SwitchTabToPayload switch_tab_to_payload = 11; + SetTimeoutPayload set_timeout_payload = 12; + ExecCmdPayload exec_cmd_payload = 13; + PluginMessagePayload post_message_to_payload = 14; + PluginMessagePayload post_message_to_plugin_payload = 15; + bool show_self_payload = 16; + SwitchToModePayload switch_to_mode_payload = 17; + string new_tabs_with_layout_payload = 18; + ResizePayload resize_payload = 19; + ResizePayload resize_with_direction_payload = 20; + MovePayload move_focus_payload = 21; + MovePayload move_focus_or_tab_payload = 22; + bytes write_payload = 23; + string write_chars_payload = 24; + MovePayload move_pane_with_direction_payload = 25; + string go_to_tab_name_payload = 26; + string focus_or_create_tab_payload = 27; + int32 go_to_tab_payload = 28; + string start_or_reload_plugin_payload = 29; + int32 close_terminal_pane_payload = 30; + int32 close_plugin_pane_payload = 31; + PaneIdAndShouldFloat focus_terminal_pane_payload = 32; + PaneIdAndShouldFloat focus_plugin_pane_payload = 33; + IdAndNewName rename_terminal_pane_payload = 34; + IdAndNewName rename_plugin_pane_payload = 35; + IdAndNewName rename_tab_payload = 36; + string report_crash_payload = 37; + } +} + +message SubscribePayload { + event.EventNameList subscriptions = 1; +} + +message UnsubscribePayload { + event.EventNameList subscriptions = 1; +} + +message OpenFilePayload { + file.File file_to_open = 1; +} + +message OpenCommandPanePayload { + command.Command command_to_run = 1; +} + +message SwitchTabToPayload { + int32 tab_index = 1; +} + +message SetTimeoutPayload { + float seconds = 1; +} + +message ExecCmdPayload { + repeated string command_line = 1; +} + +message PluginMessagePayload { + api.message.Message message = 1; +} + +message SwitchToModePayload { + input_mode.InputModeMessage input_mode = 1; +} + +message ResizePayload { + resize.Resize resize = 1; +} + +message MovePayload { + resize.MoveDirection direction = 1; +} + +message PaneIdAndShouldFloat { + int32 pane_id = 1; + bool should_float = 2; +} + +message IdAndNewName { + int32 id = 1; // pane id or tab index + string new_name = 2; +} diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs new file mode 100644 index 0000000000..bf359dd202 --- /dev/null +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -0,0 +1,825 @@ +pub use super::generated_api::api::{ + event::EventNameList as ProtobufEventNameList, + plugin_command::{ + plugin_command::Payload, CommandName, ExecCmdPayload, IdAndNewName, MovePayload, + OpenCommandPanePayload, OpenFilePayload, PaneIdAndShouldFloat, + PluginCommand as ProtobufPluginCommand, PluginMessagePayload, ResizePayload, + SetTimeoutPayload, SubscribePayload, SwitchTabToPayload, SwitchToModePayload, + UnsubscribePayload, + }, + resize::ResizeAction as ProtobufResizeAction, +}; + +use crate::data::PluginCommand; + +use std::convert::TryFrom; + +impl TryFrom for PluginCommand { + type Error = &'static str; + fn try_from(protobuf_plugin_command: ProtobufPluginCommand) -> Result { + match CommandName::from_i32(protobuf_plugin_command.name) { + Some(CommandName::Subscribe) => match protobuf_plugin_command.payload { + Some(Payload::SubscribePayload(subscribe_payload)) => { + let protobuf_event_list = subscribe_payload.subscriptions; + match protobuf_event_list { + Some(protobuf_event_list) => { + Ok(PluginCommand::Subscribe(protobuf_event_list.try_into()?)) + }, + None => Err("malformed subscription event"), + } + }, + _ => Err("Mismatched payload for Subscribe"), + }, + Some(CommandName::Unsubscribe) => match protobuf_plugin_command.payload { + Some(Payload::UnsubscribePayload(unsubscribe_payload)) => { + let protobuf_event_list = unsubscribe_payload.subscriptions; + match protobuf_event_list { + Some(protobuf_event_list) => { + Ok(PluginCommand::Unsubscribe(protobuf_event_list.try_into()?)) + }, + None => Err("malformed unsubscription event"), + } + }, + _ => Err("Mismatched payload for Unsubscribe"), + }, + Some(CommandName::SetSelectable) => match protobuf_plugin_command.payload { + Some(Payload::SetSelectablePayload(should_be_selectable)) => { + Ok(PluginCommand::SetSelectable(should_be_selectable)) + }, + _ => Err("Mismatched payload for SetSelectable"), + }, + Some(CommandName::GetPluginIds) => { + if protobuf_plugin_command.payload.is_some() { + Err("GetPluginIds should not have a payload") + } else { + Ok(PluginCommand::GetPluginIds) + } + }, + Some(CommandName::GetZellijVersion) => { + if protobuf_plugin_command.payload.is_some() { + Err("GetZellijVersion should not have a payload") + } else { + Ok(PluginCommand::GetZellijVersion) + } + }, + Some(CommandName::OpenFile) => match protobuf_plugin_command.payload { + Some(Payload::OpenFilePayload(file_to_open_payload)) => { + match file_to_open_payload.file_to_open { + Some(file_to_open) => Ok(PluginCommand::OpenFile(file_to_open.try_into()?)), + None => Err("Malformed open file payload"), + } + }, + _ => Err("Mismatched payload for OpenFile"), + }, + Some(CommandName::OpenFileFloating) => match protobuf_plugin_command.payload { + Some(Payload::OpenFileFloatingPayload(file_to_open_payload)) => { + match file_to_open_payload.file_to_open { + Some(file_to_open) => { + Ok(PluginCommand::OpenFileFloating(file_to_open.try_into()?)) + }, + None => Err("Malformed open file payload"), + } + }, + _ => Err("Mismatched payload for OpenFile"), + }, + Some(CommandName::OpenTerminal) => match protobuf_plugin_command.payload { + Some(Payload::OpenTerminalPayload(file_to_open_payload)) => { + match file_to_open_payload.file_to_open { + Some(file_to_open) => { + Ok(PluginCommand::OpenTerminal(file_to_open.try_into()?)) + }, + None => Err("Malformed open terminal payload"), + } + }, + _ => Err("Mismatched payload for OpenTerminal"), + }, + Some(CommandName::OpenTerminalFloating) => match protobuf_plugin_command.payload { + Some(Payload::OpenTerminalFloatingPayload(file_to_open_payload)) => { + match file_to_open_payload.file_to_open { + Some(file_to_open) => Ok(PluginCommand::OpenTerminalFloating( + file_to_open.try_into()?, + )), + None => Err("Malformed open terminal floating payload"), + } + }, + _ => Err("Mismatched payload for OpenTerminalFloating"), + }, + Some(CommandName::OpenCommandPane) => match protobuf_plugin_command.payload { + Some(Payload::OpenCommandPanePayload(command_to_run_payload)) => { + match command_to_run_payload.command_to_run { + Some(command_to_run) => { + Ok(PluginCommand::OpenCommandPane(command_to_run.try_into()?)) + }, + None => Err("Malformed open open command pane payload"), + } + }, + _ => Err("Mismatched payload for OpenCommandPane"), + }, + Some(CommandName::OpenCommandPaneFloating) => match protobuf_plugin_command.payload { + Some(Payload::OpenCommandPaneFloatingPayload(command_to_run_payload)) => { + match command_to_run_payload.command_to_run { + Some(command_to_run) => Ok(PluginCommand::OpenCommandPaneFloating( + command_to_run.try_into()?, + )), + None => Err("Malformed open command pane floating payload"), + } + }, + _ => Err("Mismatched payload for OpenCommandPaneFloating"), + }, + Some(CommandName::SwitchTabTo) => match protobuf_plugin_command.payload { + Some(Payload::SwitchTabToPayload(switch_to_tab_payload)) => Ok( + PluginCommand::SwitchTabTo(switch_to_tab_payload.tab_index as u32), + ), + _ => Err("Mismatched payload for SwitchToTab"), + }, + Some(CommandName::SetTimeout) => match protobuf_plugin_command.payload { + Some(Payload::SetTimeoutPayload(set_timeout_payload)) => { + Ok(PluginCommand::SetTimeout(set_timeout_payload.seconds)) + }, + _ => Err("Mismatched payload for SetTimeout"), + }, + Some(CommandName::ExecCmd) => match protobuf_plugin_command.payload { + Some(Payload::ExecCmdPayload(exec_cmd_payload)) => { + Ok(PluginCommand::ExecCmd(exec_cmd_payload.command_line)) + }, + _ => Err("Mismatched payload for ExecCmd"), + }, + Some(CommandName::PostMessageTo) => match protobuf_plugin_command.payload { + Some(Payload::PostMessageToPayload(post_message_to_payload)) => { + match post_message_to_payload.message { + Some(message) => Ok(PluginCommand::PostMessageTo(message.try_into()?)), + None => Err("Malformed post message to payload"), + } + }, + _ => Err("Mismatched payload for PostMessageTo"), + }, + Some(CommandName::PostMessageToPlugin) => match protobuf_plugin_command.payload { + Some(Payload::PostMessageToPluginPayload(post_message_to_payload)) => { + match post_message_to_payload.message { + Some(message) => { + Ok(PluginCommand::PostMessageToPlugin(message.try_into()?)) + }, + None => Err("Malformed post message to plugin payload"), + } + }, + _ => Err("Mismatched payload for PostMessageToPlugin"), + }, + Some(CommandName::HideSelf) => { + if protobuf_plugin_command.payload.is_some() { + return Err("HideSelf should not have a payload"); + } + Ok(PluginCommand::HideSelf) + }, + Some(CommandName::ShowSelf) => match protobuf_plugin_command.payload { + Some(Payload::ShowSelfPayload(should_float_if_hidden)) => { + Ok(PluginCommand::ShowSelf(should_float_if_hidden)) + }, + _ => Err("Mismatched payload for ShowSelf"), + }, + Some(CommandName::SwitchToMode) => match protobuf_plugin_command.payload { + Some(Payload::SwitchToModePayload(switch_to_mode_payload)) => { + match switch_to_mode_payload.input_mode { + Some(input_mode) => Ok(PluginCommand::SwitchToMode(input_mode.try_into()?)), + None => Err("Malformed switch to mode payload"), + } + }, + _ => Err("Mismatched payload for SwitchToMode"), + }, + Some(CommandName::NewTabsWithLayout) => match protobuf_plugin_command.payload { + Some(Payload::NewTabsWithLayoutPayload(raw_layout)) => { + Ok(PluginCommand::NewTabsWithLayout(raw_layout)) + }, + _ => Err("Mismatched payload for NewTabsWithLayout"), + }, + Some(CommandName::NewTab) => { + if protobuf_plugin_command.payload.is_some() { + return Err("NewTab should not have a payload"); + } + Ok(PluginCommand::NewTab) + }, + Some(CommandName::GoToNextTab) => { + if protobuf_plugin_command.payload.is_some() { + return Err("GoToNextTab should not have a payload"); + } + Ok(PluginCommand::GoToNextTab) + }, + Some(CommandName::GoToPreviousTab) => { + if protobuf_plugin_command.payload.is_some() { + return Err("GoToPreviousTab should not have a payload"); + } + Ok(PluginCommand::GoToPreviousTab) + }, + Some(CommandName::Resize) => match protobuf_plugin_command.payload { + Some(Payload::ResizePayload(resize_payload)) => match resize_payload.resize { + Some(resize) => Ok(PluginCommand::Resize(resize.try_into()?)), + None => Err("Malformed switch resize payload"), + }, + _ => Err("Mismatched payload for Resize"), + }, + Some(CommandName::ResizeWithDirection) => match protobuf_plugin_command.payload { + Some(Payload::ResizeWithDirectionPayload(resize_with_direction_payload)) => { + match resize_with_direction_payload.resize { + Some(resize) => Ok(PluginCommand::ResizeWithDirection(resize.try_into()?)), + None => Err("Malformed switch resize payload"), + } + }, + _ => Err("Mismatched payload for Resize"), + }, + Some(CommandName::FocusNextPane) => { + if protobuf_plugin_command.payload.is_some() { + return Err("FocusNextPane should not have a payload"); + } + Ok(PluginCommand::FocusNextPane) + }, + Some(CommandName::FocusPreviousPane) => { + if protobuf_plugin_command.payload.is_some() { + return Err("FocusPreviousPane should not have a payload"); + } + Ok(PluginCommand::FocusPreviousPane) + }, + Some(CommandName::MoveFocus) => match protobuf_plugin_command.payload { + Some(Payload::MoveFocusPayload(move_payload)) => match move_payload.direction { + Some(direction) => Ok(PluginCommand::MoveFocus(direction.try_into()?)), + None => Err("Malformed move focus payload"), + }, + _ => Err("Mismatched payload for MoveFocus"), + }, + Some(CommandName::MoveFocusOrTab) => match protobuf_plugin_command.payload { + Some(Payload::MoveFocusOrTabPayload(move_payload)) => { + match move_payload.direction { + Some(direction) => Ok(PluginCommand::MoveFocusOrTab(direction.try_into()?)), + None => Err("Malformed move focus or tab payload"), + } + }, + _ => Err("Mismatched payload for MoveFocusOrTab"), + }, + Some(CommandName::Detach) => { + if protobuf_plugin_command.payload.is_some() { + return Err("Detach should not have a payload"); + } + Ok(PluginCommand::Detach) + }, + Some(CommandName::EditScrollback) => { + if protobuf_plugin_command.payload.is_some() { + return Err("EditScrollback should not have a payload"); + } + Ok(PluginCommand::EditScrollback) + }, + Some(CommandName::Write) => match protobuf_plugin_command.payload { + Some(Payload::WritePayload(bytes)) => Ok(PluginCommand::Write(bytes)), + _ => Err("Mismatched payload for Write"), + }, + Some(CommandName::WriteChars) => match protobuf_plugin_command.payload { + Some(Payload::WriteCharsPayload(chars)) => Ok(PluginCommand::WriteChars(chars)), + _ => Err("Mismatched payload for WriteChars"), + }, + Some(CommandName::ToggleTab) => { + if protobuf_plugin_command.payload.is_some() { + return Err("ToggleTab should not have a payload"); + } + Ok(PluginCommand::ToggleTab) + }, + Some(CommandName::MovePane) => { + if protobuf_plugin_command.payload.is_some() { + return Err("MovePane should not have a payload"); + } + Ok(PluginCommand::MovePane) + }, + Some(CommandName::MovePaneWithDirection) => match protobuf_plugin_command.payload { + Some(Payload::MovePaneWithDirectionPayload(move_payload)) => { + match move_payload.direction { + Some(direction) => { + Ok(PluginCommand::MovePaneWithDirection(direction.try_into()?)) + }, + None => Err("Malformed MovePaneWithDirection payload"), + } + }, + _ => Err("Mismatched payload for MovePaneWithDirection"), + }, + Some(CommandName::ClearScreen) => { + if protobuf_plugin_command.payload.is_some() { + return Err("ClearScreen should not have a payload"); + } + Ok(PluginCommand::ClearScreen) + }, + Some(CommandName::ScrollUp) => { + if protobuf_plugin_command.payload.is_some() { + return Err("ScrollUp should not have a payload"); + } + Ok(PluginCommand::ScrollUp) + }, + Some(CommandName::ScrollDown) => { + if protobuf_plugin_command.payload.is_some() { + return Err("ScrollDown should not have a payload"); + } + Ok(PluginCommand::ScrollDown) + }, + Some(CommandName::ScrollToTop) => { + if protobuf_plugin_command.payload.is_some() { + return Err("ScrollToTop should not have a payload"); + } + Ok(PluginCommand::ScrollToTop) + }, + Some(CommandName::ScrollToBottom) => { + if protobuf_plugin_command.payload.is_some() { + return Err("ScrollToBottom should not have a payload"); + } + Ok(PluginCommand::ScrollToBottom) + }, + Some(CommandName::PageScrollUp) => { + if protobuf_plugin_command.payload.is_some() { + return Err("PageScrollUp should not have a payload"); + } + Ok(PluginCommand::PageScrollUp) + }, + Some(CommandName::PageScrollDown) => { + if protobuf_plugin_command.payload.is_some() { + return Err("PageScrollDown should not have a payload"); + } + Ok(PluginCommand::PageScrollDown) + }, + Some(CommandName::ToggleFocusFullscreen) => { + if protobuf_plugin_command.payload.is_some() { + return Err("ToggleFocusFullscreen should not have a payload"); + } + Ok(PluginCommand::ToggleFocusFullscreen) + }, + Some(CommandName::TogglePaneFrames) => { + if protobuf_plugin_command.payload.is_some() { + return Err("TogglePaneFrames should not have a payload"); + } + Ok(PluginCommand::TogglePaneFrames) + }, + Some(CommandName::TogglePaneEmbedOrEject) => { + if protobuf_plugin_command.payload.is_some() { + return Err("TogglePaneEmbedOrEject should not have a payload"); + } + Ok(PluginCommand::TogglePaneEmbedOrEject) + }, + Some(CommandName::UndoRenamePane) => { + if protobuf_plugin_command.payload.is_some() { + return Err("UndoRenamePane should not have a payload"); + } + Ok(PluginCommand::UndoRenamePane) + }, + Some(CommandName::CloseFocus) => { + if protobuf_plugin_command.payload.is_some() { + return Err("CloseFocus should not have a payload"); + } + Ok(PluginCommand::CloseFocus) + }, + Some(CommandName::ToggleActiveTabSync) => { + if protobuf_plugin_command.payload.is_some() { + return Err("ToggleActiveTabSync should not have a payload"); + } + Ok(PluginCommand::ToggleActiveTabSync) + }, + Some(CommandName::CloseFocusedTab) => { + if protobuf_plugin_command.payload.is_some() { + return Err("CloseFocusedTab should not have a payload"); + } + Ok(PluginCommand::CloseFocusedTab) + }, + Some(CommandName::UndoRenameTab) => { + if protobuf_plugin_command.payload.is_some() { + return Err("UndoRenameTab should not have a payload"); + } + Ok(PluginCommand::UndoRenameTab) + }, + Some(CommandName::QuitZellij) => { + if protobuf_plugin_command.payload.is_some() { + return Err("QuitZellij should not have a payload"); + } + Ok(PluginCommand::QuitZellij) + }, + Some(CommandName::PreviousSwapLayout) => { + if protobuf_plugin_command.payload.is_some() { + return Err("PreviousSwapLayout should not have a payload"); + } + Ok(PluginCommand::PreviousSwapLayout) + }, + Some(CommandName::NextSwapLayout) => { + if protobuf_plugin_command.payload.is_some() { + return Err("NextSwapLayout should not have a payload"); + } + Ok(PluginCommand::NextSwapLayout) + }, + Some(CommandName::GoToTabName) => match protobuf_plugin_command.payload { + Some(Payload::GoToTabNamePayload(tab_name)) => { + Ok(PluginCommand::GoToTabName(tab_name)) + }, + _ => Err("Mismatched payload for GoToTabName"), + }, + Some(CommandName::FocusOrCreateTab) => match protobuf_plugin_command.payload { + Some(Payload::FocusOrCreateTabPayload(tab_name)) => { + Ok(PluginCommand::FocusOrCreateTab(tab_name)) + }, + _ => Err("Mismatched payload for FocusOrCreateTab"), + }, + Some(CommandName::GoToTab) => match protobuf_plugin_command.payload { + Some(Payload::GoToTabPayload(tab_index)) => { + Ok(PluginCommand::GoToTab(tab_index as u32)) + }, + _ => Err("Mismatched payload for GoToTab"), + }, + Some(CommandName::StartOrReloadPlugin) => match protobuf_plugin_command.payload { + Some(Payload::StartOrReloadPluginPayload(url)) => { + Ok(PluginCommand::StartOrReloadPlugin(url)) + }, + _ => Err("Mismatched payload for StartOrReloadPlugin"), + }, + Some(CommandName::CloseTerminalPane) => match protobuf_plugin_command.payload { + Some(Payload::CloseTerminalPanePayload(pane_id)) => { + Ok(PluginCommand::CloseTerminalPane(pane_id as u32)) + }, + _ => Err("Mismatched payload for CloseTerminalPane"), + }, + Some(CommandName::ClosePluginPane) => match protobuf_plugin_command.payload { + Some(Payload::ClosePluginPanePayload(pane_id)) => { + Ok(PluginCommand::ClosePluginPane(pane_id as u32)) + }, + _ => Err("Mismatched payload for ClosePluginPane"), + }, + Some(CommandName::FocusTerminalPane) => match protobuf_plugin_command.payload { + Some(Payload::FocusTerminalPanePayload(payload)) => { + let pane_id = payload.pane_id as u32; + let should_float = payload.should_float; + Ok(PluginCommand::FocusTerminalPane(pane_id, should_float)) + }, + _ => Err("Mismatched payload for ClosePluginPane"), + }, + Some(CommandName::FocusPluginPane) => match protobuf_plugin_command.payload { + Some(Payload::FocusPluginPanePayload(payload)) => { + let pane_id = payload.pane_id as u32; + let should_float = payload.should_float; + Ok(PluginCommand::FocusPluginPane(pane_id, should_float)) + }, + _ => Err("Mismatched payload for ClosePluginPane"), + }, + Some(CommandName::RenameTerminalPane) => match protobuf_plugin_command.payload { + Some(Payload::RenameTerminalPanePayload(payload)) => { + let pane_id = payload.id as u32; + let new_name = payload.new_name; + Ok(PluginCommand::RenameTerminalPane(pane_id, new_name)) + }, + _ => Err("Mismatched payload for RenameTerminalPane"), + }, + Some(CommandName::RenamePluginPane) => match protobuf_plugin_command.payload { + Some(Payload::RenamePluginPanePayload(payload)) => { + let pane_id = payload.id as u32; + let new_name = payload.new_name; + Ok(PluginCommand::RenamePluginPane(pane_id, new_name)) + }, + _ => Err("Mismatched payload for RenamePluginPane"), + }, + Some(CommandName::RenameTab) => match protobuf_plugin_command.payload { + Some(Payload::RenameTabPayload(payload)) => { + let tab_index = payload.id as u32; + let name = payload.new_name; + Ok(PluginCommand::RenameTab(tab_index, name)) + }, + _ => Err("Mismatched payload for RenameTab"), + }, + Some(CommandName::ReportCrash) => match protobuf_plugin_command.payload { + Some(Payload::ReportCrashPayload(payload)) => { + Ok(PluginCommand::ReportPanic(payload)) + }, + _ => Err("Mismatched payload for ReportCrash"), + }, + None => Err("Unrecognized plugin command"), + } + } +} + +impl TryFrom for ProtobufPluginCommand { + type Error = &'static str; + fn try_from(plugin_command: PluginCommand) -> Result { + match plugin_command { + PluginCommand::Subscribe(subscriptions) => { + let subscriptions: ProtobufEventNameList = subscriptions.try_into()?; + Ok(ProtobufPluginCommand { + name: CommandName::Subscribe as i32, + payload: Some(Payload::SubscribePayload(SubscribePayload { + subscriptions: Some(subscriptions), + })), + }) + }, + PluginCommand::Unsubscribe(subscriptions) => { + let subscriptions: ProtobufEventNameList = subscriptions.try_into()?; + Ok(ProtobufPluginCommand { + name: CommandName::Unsubscribe as i32, + payload: Some(Payload::UnsubscribePayload(UnsubscribePayload { + subscriptions: Some(subscriptions), + })), + }) + }, + PluginCommand::SetSelectable(should_be_selectable) => Ok(ProtobufPluginCommand { + name: CommandName::SetSelectable as i32, + payload: Some(Payload::SetSelectablePayload(should_be_selectable)), + }), + PluginCommand::GetPluginIds => Ok(ProtobufPluginCommand { + name: CommandName::GetPluginIds as i32, + payload: None, + }), + PluginCommand::GetZellijVersion => Ok(ProtobufPluginCommand { + name: CommandName::GetZellijVersion as i32, + payload: None, + }), + PluginCommand::OpenFile(file_to_open) => Ok(ProtobufPluginCommand { + name: CommandName::OpenFile as i32, + payload: Some(Payload::OpenFilePayload(OpenFilePayload { + file_to_open: Some(file_to_open.try_into()?), + })), + }), + PluginCommand::OpenFileFloating(file_to_open) => Ok(ProtobufPluginCommand { + name: CommandName::OpenFileFloating as i32, + payload: Some(Payload::OpenFileFloatingPayload(OpenFilePayload { + file_to_open: Some(file_to_open.try_into()?), + })), + }), + PluginCommand::OpenTerminal(cwd) => Ok(ProtobufPluginCommand { + name: CommandName::OpenTerminal as i32, + payload: Some(Payload::OpenTerminalPayload(OpenFilePayload { + file_to_open: Some(cwd.try_into()?), + })), + }), + PluginCommand::OpenTerminalFloating(cwd) => Ok(ProtobufPluginCommand { + name: CommandName::OpenTerminalFloating as i32, + payload: Some(Payload::OpenTerminalFloatingPayload(OpenFilePayload { + file_to_open: Some(cwd.try_into()?), + })), + }), + PluginCommand::OpenCommandPane(command_to_run) => Ok(ProtobufPluginCommand { + name: CommandName::OpenCommandPane as i32, + payload: Some(Payload::OpenCommandPanePayload(OpenCommandPanePayload { + command_to_run: Some(command_to_run.try_into()?), + })), + }), + PluginCommand::OpenCommandPaneFloating(command_to_run) => Ok(ProtobufPluginCommand { + name: CommandName::OpenCommandPaneFloating as i32, + payload: Some(Payload::OpenCommandPaneFloatingPayload( + OpenCommandPanePayload { + command_to_run: Some(command_to_run.try_into()?), + }, + )), + }), + PluginCommand::SwitchTabTo(tab_index) => Ok(ProtobufPluginCommand { + name: CommandName::SwitchTabTo as i32, + payload: Some(Payload::SwitchTabToPayload(SwitchTabToPayload { + tab_index: tab_index as i32, + })), + }), + PluginCommand::SetTimeout(seconds) => Ok(ProtobufPluginCommand { + name: CommandName::SetTimeout as i32, + payload: Some(Payload::SetTimeoutPayload(SetTimeoutPayload { seconds })), + }), + PluginCommand::ExecCmd(command_line) => Ok(ProtobufPluginCommand { + name: CommandName::ExecCmd as i32, + payload: Some(Payload::ExecCmdPayload(ExecCmdPayload { command_line })), + }), + PluginCommand::PostMessageTo(plugin_message) => Ok(ProtobufPluginCommand { + name: CommandName::PostMessageTo as i32, + payload: Some(Payload::PostMessageToPayload(PluginMessagePayload { + message: Some(plugin_message.try_into()?), + })), + }), + PluginCommand::PostMessageToPlugin(plugin_message) => Ok(ProtobufPluginCommand { + name: CommandName::PostMessageToPlugin as i32, + payload: Some(Payload::PostMessageToPluginPayload(PluginMessagePayload { + message: Some(plugin_message.try_into()?), + })), + }), + PluginCommand::HideSelf => Ok(ProtobufPluginCommand { + name: CommandName::HideSelf as i32, + payload: None, + }), + PluginCommand::ShowSelf(should_float_if_hidden) => Ok(ProtobufPluginCommand { + name: CommandName::ShowSelf as i32, + payload: Some(Payload::ShowSelfPayload(should_float_if_hidden)), + }), + PluginCommand::SwitchToMode(input_mode) => Ok(ProtobufPluginCommand { + name: CommandName::SwitchToMode as i32, + payload: Some(Payload::SwitchToModePayload(SwitchToModePayload { + input_mode: Some(input_mode.try_into()?), + })), + }), + PluginCommand::NewTabsWithLayout(raw_layout) => Ok(ProtobufPluginCommand { + name: CommandName::NewTabsWithLayout as i32, + payload: Some(Payload::NewTabsWithLayoutPayload(raw_layout)), + }), + PluginCommand::NewTab => Ok(ProtobufPluginCommand { + name: CommandName::NewTab as i32, + payload: None, + }), + PluginCommand::GoToNextTab => Ok(ProtobufPluginCommand { + name: CommandName::GoToNextTab as i32, + payload: None, + }), + PluginCommand::GoToPreviousTab => Ok(ProtobufPluginCommand { + name: CommandName::GoToPreviousTab as i32, + payload: None, + }), + PluginCommand::Resize(resize) => Ok(ProtobufPluginCommand { + name: CommandName::Resize as i32, + payload: Some(Payload::ResizePayload(ResizePayload { + resize: Some(resize.try_into()?), + })), + }), + PluginCommand::ResizeWithDirection(resize) => Ok(ProtobufPluginCommand { + name: CommandName::ResizeWithDirection as i32, + payload: Some(Payload::ResizeWithDirectionPayload(ResizePayload { + resize: Some(resize.try_into()?), + })), + }), + PluginCommand::FocusNextPane => Ok(ProtobufPluginCommand { + name: CommandName::FocusNextPane as i32, + payload: None, + }), + PluginCommand::FocusPreviousPane => Ok(ProtobufPluginCommand { + name: CommandName::FocusPreviousPane as i32, + payload: None, + }), + PluginCommand::MoveFocus(direction) => Ok(ProtobufPluginCommand { + name: CommandName::MoveFocus as i32, + payload: Some(Payload::MoveFocusPayload(MovePayload { + direction: Some(direction.try_into()?), + })), + }), + PluginCommand::MoveFocusOrTab(direction) => Ok(ProtobufPluginCommand { + name: CommandName::MoveFocusOrTab as i32, + payload: Some(Payload::MoveFocusOrTabPayload(MovePayload { + direction: Some(direction.try_into()?), + })), + }), + PluginCommand::Detach => Ok(ProtobufPluginCommand { + name: CommandName::Detach as i32, + payload: None, + }), + PluginCommand::EditScrollback => Ok(ProtobufPluginCommand { + name: CommandName::EditScrollback as i32, + payload: None, + }), + PluginCommand::Write(bytes) => Ok(ProtobufPluginCommand { + name: CommandName::Write as i32, + payload: Some(Payload::WritePayload(bytes)), + }), + PluginCommand::WriteChars(chars) => Ok(ProtobufPluginCommand { + name: CommandName::WriteChars as i32, + payload: Some(Payload::WriteCharsPayload(chars)), + }), + PluginCommand::ToggleTab => Ok(ProtobufPluginCommand { + name: CommandName::ToggleTab as i32, + payload: None, + }), + PluginCommand::MovePane => Ok(ProtobufPluginCommand { + name: CommandName::MovePane as i32, + payload: None, + }), + PluginCommand::MovePaneWithDirection(direction) => Ok(ProtobufPluginCommand { + name: CommandName::MovePaneWithDirection as i32, + payload: Some(Payload::MovePaneWithDirectionPayload(MovePayload { + direction: Some(direction.try_into()?), + })), + }), + PluginCommand::ClearScreen => Ok(ProtobufPluginCommand { + name: CommandName::ClearScreen as i32, + payload: None, + }), + PluginCommand::ScrollUp => Ok(ProtobufPluginCommand { + name: CommandName::ScrollUp as i32, + payload: None, + }), + PluginCommand::ScrollDown => Ok(ProtobufPluginCommand { + name: CommandName::ScrollDown as i32, + payload: None, + }), + PluginCommand::ScrollToTop => Ok(ProtobufPluginCommand { + name: CommandName::ScrollToTop as i32, + payload: None, + }), + PluginCommand::ScrollToBottom => Ok(ProtobufPluginCommand { + name: CommandName::ScrollToBottom as i32, + payload: None, + }), + PluginCommand::PageScrollUp => Ok(ProtobufPluginCommand { + name: CommandName::PageScrollUp as i32, + payload: None, + }), + PluginCommand::PageScrollDown => Ok(ProtobufPluginCommand { + name: CommandName::PageScrollDown as i32, + payload: None, + }), + PluginCommand::ToggleFocusFullscreen => Ok(ProtobufPluginCommand { + name: CommandName::ToggleFocusFullscreen as i32, + payload: None, + }), + PluginCommand::TogglePaneFrames => Ok(ProtobufPluginCommand { + name: CommandName::TogglePaneFrames as i32, + payload: None, + }), + PluginCommand::TogglePaneEmbedOrEject => Ok(ProtobufPluginCommand { + name: CommandName::TogglePaneEmbedOrEject as i32, + payload: None, + }), + PluginCommand::UndoRenamePane => Ok(ProtobufPluginCommand { + name: CommandName::UndoRenamePane as i32, + payload: None, + }), + PluginCommand::CloseFocus => Ok(ProtobufPluginCommand { + name: CommandName::CloseFocus as i32, + payload: None, + }), + PluginCommand::ToggleActiveTabSync => Ok(ProtobufPluginCommand { + name: CommandName::ToggleActiveTabSync as i32, + payload: None, + }), + PluginCommand::CloseFocusedTab => Ok(ProtobufPluginCommand { + name: CommandName::CloseFocusedTab as i32, + payload: None, + }), + PluginCommand::UndoRenameTab => Ok(ProtobufPluginCommand { + name: CommandName::UndoRenameTab as i32, + payload: None, + }), + PluginCommand::QuitZellij => Ok(ProtobufPluginCommand { + name: CommandName::QuitZellij as i32, + payload: None, + }), + PluginCommand::PreviousSwapLayout => Ok(ProtobufPluginCommand { + name: CommandName::PreviousSwapLayout as i32, + payload: None, + }), + PluginCommand::NextSwapLayout => Ok(ProtobufPluginCommand { + name: CommandName::NextSwapLayout as i32, + payload: None, + }), + PluginCommand::GoToTabName(tab_name) => Ok(ProtobufPluginCommand { + name: CommandName::GoToTabName as i32, + payload: Some(Payload::GoToTabNamePayload(tab_name)), + }), + PluginCommand::FocusOrCreateTab(tab_name) => Ok(ProtobufPluginCommand { + name: CommandName::FocusOrCreateTab as i32, + payload: Some(Payload::FocusOrCreateTabPayload(tab_name)), + }), + PluginCommand::GoToTab(tab_index) => Ok(ProtobufPluginCommand { + name: CommandName::GoToTab as i32, + payload: Some(Payload::GoToTabPayload(tab_index as i32)), + }), + PluginCommand::StartOrReloadPlugin(url) => Ok(ProtobufPluginCommand { + name: CommandName::StartOrReloadPlugin as i32, + payload: Some(Payload::StartOrReloadPluginPayload(url)), + }), + PluginCommand::CloseTerminalPane(pane_id) => Ok(ProtobufPluginCommand { + name: CommandName::CloseTerminalPane as i32, + payload: Some(Payload::CloseTerminalPanePayload(pane_id as i32)), + }), + PluginCommand::ClosePluginPane(pane_id) => Ok(ProtobufPluginCommand { + name: CommandName::ClosePluginPane as i32, + payload: Some(Payload::ClosePluginPanePayload(pane_id as i32)), + }), + PluginCommand::FocusTerminalPane(pane_id, should_float_if_hidden) => { + Ok(ProtobufPluginCommand { + name: CommandName::FocusTerminalPane as i32, + payload: Some(Payload::FocusTerminalPanePayload(PaneIdAndShouldFloat { + pane_id: pane_id as i32, + should_float: should_float_if_hidden, + })), + }) + }, + PluginCommand::FocusPluginPane(pane_id, should_float_if_hidden) => { + Ok(ProtobufPluginCommand { + name: CommandName::FocusPluginPane as i32, + payload: Some(Payload::FocusPluginPanePayload(PaneIdAndShouldFloat { + pane_id: pane_id as i32, + should_float: should_float_if_hidden, + })), + }) + }, + PluginCommand::RenameTerminalPane(pane_id, new_name) => Ok(ProtobufPluginCommand { + name: CommandName::RenameTerminalPane as i32, + payload: Some(Payload::RenameTerminalPanePayload(IdAndNewName { + id: pane_id as i32, + new_name, + })), + }), + PluginCommand::RenamePluginPane(pane_id, new_name) => Ok(ProtobufPluginCommand { + name: CommandName::RenamePluginPane as i32, + payload: Some(Payload::RenamePluginPanePayload(IdAndNewName { + id: pane_id as i32, + new_name, + })), + }), + PluginCommand::RenameTab(tab_index, new_name) => Ok(ProtobufPluginCommand { + name: CommandName::RenameTab as i32, + payload: Some(Payload::RenameTabPayload(IdAndNewName { + id: tab_index as i32, + new_name, + })), + }), + PluginCommand::ReportPanic(payload) => Ok(ProtobufPluginCommand { + name: CommandName::ReportCrash as i32, + payload: Some(Payload::ReportCrashPayload(payload)), + }), + } + } +} diff --git a/zellij-utils/src/plugin_api/plugin_ids.proto b/zellij-utils/src/plugin_api/plugin_ids.proto new file mode 100644 index 0000000000..2977dbe48f --- /dev/null +++ b/zellij-utils/src/plugin_api/plugin_ids.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package api.plugin_ids; + +message PluginIds { + int32 plugin_id = 1; + int32 zellij_pid = 2; +} + +message ZellijVersion { + string version = 1; +} diff --git a/zellij-utils/src/plugin_api/plugin_ids.rs b/zellij-utils/src/plugin_api/plugin_ids.rs new file mode 100644 index 0000000000..51f526c66b --- /dev/null +++ b/zellij-utils/src/plugin_api/plugin_ids.rs @@ -0,0 +1,35 @@ +pub use super::generated_api::api::plugin_ids::{ + PluginIds as ProtobufPluginIds, ZellijVersion as ProtobufZellijVersion, +}; +use crate::data::PluginIds; + +use std::convert::TryFrom; + +impl TryFrom for PluginIds { + type Error = &'static str; + fn try_from(protobuf_plugin_ids: ProtobufPluginIds) -> Result { + Ok(PluginIds { + plugin_id: protobuf_plugin_ids.plugin_id as u32, + zellij_pid: protobuf_plugin_ids.zellij_pid as u32, + }) + } +} + +impl TryFrom for ProtobufPluginIds { + type Error = &'static str; + fn try_from(plugin_ids: PluginIds) -> Result { + Ok(ProtobufPluginIds { + plugin_id: plugin_ids.plugin_id as i32, + zellij_pid: plugin_ids.zellij_pid as i32, + }) + } +} + +impl TryFrom<&str> for ProtobufZellijVersion { + type Error = &'static str; + fn try_from(zellij_version: &str) -> Result { + Ok(ProtobufZellijVersion { + version: zellij_version.to_owned(), + }) + } +} diff --git a/zellij-utils/src/plugin_api/resize.proto b/zellij-utils/src/plugin_api/resize.proto new file mode 100644 index 0000000000..eebac79d12 --- /dev/null +++ b/zellij-utils/src/plugin_api/resize.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package api.resize; + +enum ResizeAction { + Increase = 0; + Decrease = 1; +} + +enum ResizeDirection { + Left = 0; + Right = 1; + Up = 2; + Down = 3; +} + +message Resize { + ResizeAction resize_action = 1; + optional ResizeDirection direction = 2; +} + +message MoveDirection { + ResizeDirection direction = 1; +} diff --git a/zellij-utils/src/plugin_api/resize.rs b/zellij-utils/src/plugin_api/resize.rs new file mode 100644 index 0000000000..61031c09e6 --- /dev/null +++ b/zellij-utils/src/plugin_api/resize.rs @@ -0,0 +1,130 @@ +pub use super::generated_api::api::resize::{ + MoveDirection as ProtobufMoveDirection, Resize as ProtobufResize, ResizeAction, + ResizeDirection, ResizeDirection as ProtobufResizeDirection, +}; +use crate::data::{Direction, Resize, ResizeStrategy}; + +use std::convert::TryFrom; + +impl TryFrom for Resize { + type Error = &'static str; + fn try_from(protobuf_resize: ProtobufResize) -> Result { + if protobuf_resize.direction.is_some() { + return Err("Resize cannot have a direction"); + } + match ResizeAction::from_i32(protobuf_resize.resize_action) { + Some(ResizeAction::Increase) => Ok(Resize::Increase), + Some(ResizeAction::Decrease) => Ok(Resize::Decrease), + None => Err("No resize action for the given index"), + } + } +} + +impl TryFrom for ProtobufResize { + type Error = &'static str; + fn try_from(resize: Resize) -> Result { + Ok(ProtobufResize { + resize_action: match resize { + Resize::Increase => ResizeAction::Increase as i32, + Resize::Decrease => ResizeAction::Decrease as i32, + }, + direction: None, + }) + } +} + +impl TryFrom for ResizeStrategy { + type Error = &'static str; + fn try_from(protobuf_resize: ProtobufResize) -> Result { + let direction = match protobuf_resize + .direction + .and_then(|r| ResizeDirection::from_i32(r)) + { + Some(ResizeDirection::Left) => Some(Direction::Left), + Some(ResizeDirection::Right) => Some(Direction::Right), + Some(ResizeDirection::Up) => Some(Direction::Up), + Some(ResizeDirection::Down) => Some(Direction::Down), + None => None, + }; + let resize = match ResizeAction::from_i32(protobuf_resize.resize_action) { + Some(ResizeAction::Increase) => Resize::Increase, + Some(ResizeAction::Decrease) => Resize::Decrease, + None => return Err("No resize action for the given index"), + }; + Ok(ResizeStrategy { + direction, + resize, + invert_on_boundaries: false, + }) + } +} + +impl TryFrom for ProtobufResize { + type Error = &'static str; + fn try_from(resize_strategy: ResizeStrategy) -> Result { + Ok(ProtobufResize { + resize_action: match resize_strategy.resize { + Resize::Increase => ResizeAction::Increase as i32, + Resize::Decrease => ResizeAction::Decrease as i32, + }, + direction: match resize_strategy.direction { + Some(Direction::Left) => Some(ResizeDirection::Left as i32), + Some(Direction::Right) => Some(ResizeDirection::Right as i32), + Some(Direction::Up) => Some(ResizeDirection::Up as i32), + Some(Direction::Down) => Some(ResizeDirection::Down as i32), + None => None, + }, + }) + } +} + +impl TryFrom for Direction { + type Error = &'static str; + fn try_from(protobuf_move_direction: ProtobufMoveDirection) -> Result { + match ResizeDirection::from_i32(protobuf_move_direction.direction) { + Some(ResizeDirection::Left) => Ok(Direction::Left), + Some(ResizeDirection::Right) => Ok(Direction::Right), + Some(ResizeDirection::Up) => Ok(Direction::Up), + Some(ResizeDirection::Down) => Ok(Direction::Down), + None => Err("No direction for the given index"), + } + } +} + +impl TryFrom for ProtobufMoveDirection { + type Error = &'static str; + fn try_from(direction: Direction) -> Result { + Ok(ProtobufMoveDirection { + direction: match direction { + Direction::Left => ResizeDirection::Left as i32, + Direction::Right => ResizeDirection::Right as i32, + Direction::Up => ResizeDirection::Up as i32, + Direction::Down => ResizeDirection::Down as i32, + }, + }) + } +} + +impl TryFrom for Direction { + type Error = &'static str; + fn try_from(protobuf_resize_direction: ProtobufResizeDirection) -> Result { + match protobuf_resize_direction { + ProtobufResizeDirection::Left => Ok(Direction::Left), + ProtobufResizeDirection::Right => Ok(Direction::Right), + ProtobufResizeDirection::Up => Ok(Direction::Up), + ProtobufResizeDirection::Down => Ok(Direction::Down), + } + } +} + +impl TryFrom for ProtobufResizeDirection { + type Error = &'static str; + fn try_from(direction: Direction) -> Result { + Ok(match direction { + Direction::Left => ProtobufResizeDirection::Left, + Direction::Right => ProtobufResizeDirection::Right, + Direction::Up => ProtobufResizeDirection::Up, + Direction::Down => ProtobufResizeDirection::Down, + }) + } +} diff --git a/zellij-utils/src/plugin_api/style.proto b/zellij-utils/src/plugin_api/style.proto new file mode 100644 index 0000000000..f4f3b75889 --- /dev/null +++ b/zellij-utils/src/plugin_api/style.proto @@ -0,0 +1,54 @@ +syntax = "proto3"; + +package api.style; + +message Style { + Palette palette = 1; + bool rounded_corners = 2; + bool hide_session_name = 3; +} + +message Palette { + ThemeHue theme_hue = 1; + Color fg = 2; + Color bg = 3; + Color black = 4; + Color red = 5; + Color green = 6; + Color yellow = 7; + Color blue = 8; + Color magenta = 9; + Color cyan = 10; + Color white = 11; + Color orange = 12; + Color gray = 13; + Color purple = 14; + Color gold = 15; + Color silver = 16; + Color pink = 17; + Color brown = 18; +} + +message Color { + ColorType color_type = 1; + oneof payload { + RgbColorPayload rgb_color_payload = 2; + uint32 eight_bit_color_payload = 3; + } +} + +message RgbColorPayload { + uint32 red = 1; + uint32 green = 2; + uint32 blue = 3; +} + +enum ColorType { + Rgb = 0; + EightBit = 1; +} + +enum ThemeHue { + Dark = 0; + Light = 1; +} diff --git a/zellij-utils/src/plugin_api/style.rs b/zellij-utils/src/plugin_api/style.rs new file mode 100644 index 0000000000..1e3ba677f6 --- /dev/null +++ b/zellij-utils/src/plugin_api/style.rs @@ -0,0 +1,213 @@ +use super::generated_api::api::style::{ + color::Payload as ProtobufColorPayload, Color as ProtobufColor, ColorType as ProtobufColorType, + Palette as ProtobufPalette, RgbColorPayload as ProtobufRgbColorPayload, Style as ProtobufStyle, + ThemeHue as ProtobufThemeHue, +}; +use crate::data::{Palette, PaletteColor, Style, ThemeHue}; +use crate::errors::prelude::*; + +use std::convert::TryFrom; + +impl TryFrom for Style { + type Error = &'static str; + fn try_from(protobuf_style: ProtobufStyle) -> Result { + Ok(Style { + colors: protobuf_style + .palette + .ok_or("malformed style payload")? + .try_into()?, + rounded_corners: protobuf_style.rounded_corners, + hide_session_name: protobuf_style.hide_session_name, + }) + } +} + +impl TryFrom