diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-06-29 18:10:59 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-29 18:10:59 +0200 |
| commit | 245f5141c9bb7eb91f003efac4defc94470243fd (patch) | |
| tree | 52ae6d003366f97e41c8a1450602aef63c8fdca2 /zellij-utils/src | |
| parent | feat: reload config on theme change (#5135) (diff) | |
| download | sl0ppij-245f5141c9bb7eb91f003efac4defc94470243fd.tar.gz sl0ppij-245f5141c9bb7eb91f003efac4defc94470243fd.tar.xz sl0ppij-245f5141c9bb7eb91f003efac4defc94470243fd.zip | |
Title frames (#5318)
* initial implementation
* improve single line title design
* hint text plugin infra
* add proto regen task
* display alt text in status bar
* don't display text with full frames
* style alt hover text
* fix + style control own pane hover
* make multiplayer indications responsive
* update snapshots
* plugin api
* single pane as tab
* better scroll styling
* fix tests
* tab-bar plus hover
* plugin surface tests
* integration tests
* adjust all integration tests
* reduce hover noise
* add pane id to in place
* more integration tests
* style: rustfmt
* update plugin assets
* update assets and snapshots
* add pr
Diffstat (limited to 'zellij-utils/src')
30 files changed, 808 insertions, 16 deletions
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index a00c4d05..ee2b6da1 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -2,7 +2,10 @@ use crate::data::{Direction, InputMode, Resize, UnblockCondition}; use crate::setup::Setup; use crate::{ consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV}, - input::{layout::PluginUserConfiguration, options::Options}, + input::{ + layout::PluginUserConfiguration, + options::{Options, PaneFrameStyle}, + }, }; use clap::{ArgEnum, Args, Parser, Subcommand}; use serde::{Deserialize, Serialize}; @@ -884,6 +887,10 @@ pub enum CliAction { }, /// Toggle frames around panes in the UI TogglePaneFrames, + SetPaneFrameStyle { + #[clap(arg_enum, value_parser)] + style: PaneFrameStyle, + }, /// Toggle between sending text commands to all panes on the current tab and normal mode. ToggleActiveSyncTab { /// Target a specific tab by ID @@ -934,6 +941,16 @@ pub enum CliAction { )] close_replaced_pane: bool, + /// The pane to replace when opening in place, eg. terminal_1, plugin_2 or 3 (only + /// effective with --in-place; defaults to the focused pane) + #[clap( + long, + value_parser, + requires("in-place"), + conflicts_with("near-current-pane") + )] + pane_id: Option<String>, + /// Name of the new pane #[clap(short, long, value_parser)] name: Option<String>, diff --git a/zellij-utils/src/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto index 718cc279..6a7809c8 100644 --- a/zellij-utils/src/client_server_contract/common_types.proto +++ b/zellij-utils/src/client_server_contract/common_types.proto @@ -279,6 +279,7 @@ message Action { ToggleThemeAction toggle_theme = 139; ToggleMobileModeAction toggle_mobile_mode = 140; FocusLastPaneAction focus_last_pane = 141; + SetPaneFrameStyleAction set_pane_frame_style = 142; } } @@ -307,6 +308,9 @@ message HalfPageScrollUpAction {} message HalfPageScrollDownAction {} message ToggleFocusFullscreenAction {} message TogglePaneFramesAction {} +message SetPaneFrameStyleAction { + string style = 1; +} message ToggleActiveSyncTabAction {} message TogglePaneEmbedOrFloatingAction {} message ToggleFloatingPanesAction {} @@ -1183,6 +1187,7 @@ message Options { optional MobileLayout mobile_layout = 48; optional uint32 mobile_threshold_cols = 49; optional uint32 mobile_threshold_rows = 50; + optional string pane_frame_style = 51; } enum OnForceClose { diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index c11f777f..758e2ab3 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -5,6 +5,7 @@ use crate::input::keybinds::Keybinds; use crate::input::layout::{ Layout, PercentOrFixed, Run, RunPlugin, RunPluginLocation, RunPluginOrAlias, }; +pub use crate::input::options::PaneFrameStyle; use crate::pane_size::{PaneGeom, Size}; use crate::position::Position; use crate::shared::{colors as default_colors, eightbit_to_rgb}; @@ -938,6 +939,12 @@ impl From<Metadata> for FileMetadata { } } +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct StyledText { + pub text: String, + pub indices: Vec<Vec<usize>>, +} + /// These events can be subscribed to with subscribe method exported by `zellij-tile`. /// Once subscribed to, they will trigger the `update` method of the `ZellijPlugin` trait. #[derive(Debug, Clone, PartialEq, EnumDiscriminants, Display, Serialize, Deserialize)] @@ -1031,6 +1038,8 @@ pub enum Event { /// The host terminal indicated its color palette theme mode (CSI 2031 / DSR 997). HostTerminalThemeChanged(HostTerminalThemeMode), SoftKeyboardVisibilityChanged(bool), + HintText(BTreeMap<usize, StyledText>), + ActivePaneScroll(Option<(usize, usize)>), } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -1741,6 +1750,7 @@ pub struct ModeInfo { pub web_server_ip: Option<IpAddr>, pub web_server_port: Option<u16>, pub web_server_capability: Option<bool>, + pub pane_frame_style: Option<PaneFrameStyle>, } impl ModeInfo { @@ -3378,6 +3388,10 @@ pub enum PluginCommand { NewTiledPaneInTab { tab_position: usize, }, + ToggleFloatingPanes { + tab_id: Option<u64>, + }, + NewPane, GoToNextTab, GoToPreviousTab, Resize(Resize), @@ -3403,6 +3417,7 @@ pub enum PluginCommand { PageScrollDown, ToggleFocusFullscreen, TogglePaneFrames, + SetPaneFrameStyle(PaneFrameStyle), TogglePaneEmbedOrEject, UndoRenamePane, CloseFocus, diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index c9e8e25f..8ffe0907 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -282,6 +282,7 @@ pub enum ScreenContext { ToggleActiveSyncTab, ToggleActiveTerminalFullscreen, TogglePaneFrames, + SetPaneFrameStyle, SetSelectable, ShowPluginCursor, SetInvisibleBorders, @@ -466,6 +467,7 @@ pub enum ScreenContext { MoveTabWithTabId, PluginSubscribedToAnsiPaneContents, UpdateBackgroundPluginSubscriptions, + ClearHintTextCache, BroadcastModeUpdate, EnterMobileMode, ExitMobileMode, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 40158b8b..757e99e4 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -14,7 +14,7 @@ use crate::data::{FloatingPaneCoordinates, InputMode}; use crate::home::{find_default_config_dir, get_layout_dir}; use crate::input::config::{Config, ConfigError, KdlError}; use crate::input::mouse::MouseEvent; -use crate::input::options::OnForceClose; +use crate::input::options::{OnForceClose, PaneFrameStyle}; use miette::{NamedSource, Report}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -218,6 +218,7 @@ pub enum Action { ToggleFocusFullscreen, /// Toggle frames around panes in the UI TogglePaneFrames, + SetPaneFrameStyle(PaneFrameStyle), /// Toggle between sending text commands to all panes on the current tab and normal mode. ToggleActiveSyncTab, /// Open a new pane in the specified direction (relative to focus). @@ -1007,6 +1008,7 @@ impl Action { None => Ok(vec![Action::ToggleFocusFullscreen]), }, CliAction::TogglePaneFrames => Ok(vec![Action::TogglePaneFrames]), + CliAction::SetPaneFrameStyle { style } => Ok(vec![Action::SetPaneFrameStyle(style)]), CliAction::ToggleActiveSyncTab { tab_id } => match tab_id { Some(id) => Ok(vec![Action::ToggleActiveSyncTabByTabId { id: id as u64 }]), None => Ok(vec![Action::ToggleActiveSyncTab]), @@ -1019,6 +1021,7 @@ impl Action { floating, in_place, close_replaced_pane, + pane_id, name, close_on_exit, start_suspended, @@ -1039,6 +1042,18 @@ impl Action { borderless, tab_id, } => { + let pane_id_to_replace = match pane_id { + Some(pane_id_str) => match PaneId::from_str(&pane_id_str) { + Ok(parsed_pane_id) => Some(parsed_pane_id), + Err(_e) => { + return Err(format!( + "Malformed pane id: {}, expecting either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)", + pane_id_str + )) + }, + }, + None => None, + }; let current_dir = get_current_dir(); // cwd should only be specified in a plugin alias if it was explicitly given to us, // otherwise the current_dir might override a cwd defined in the alias itself @@ -1087,7 +1102,7 @@ impl Action { )) } else if in_place { NewPanePlacement::InPlace { - pane_id_to_replace: None, + pane_id_to_replace, close_replaced_pane, borderless, } @@ -1197,7 +1212,7 @@ impl Action { command: Some(run_command_action), pane_name: name, near_current_pane, - pane_id_to_replace: None, // TODO: support this + pane_id_to_replace, close_replaced_pane, tab_id, }]) @@ -1234,7 +1249,7 @@ impl Action { command: None, pane_name: name, near_current_pane, - pane_id_to_replace: None, // TODO: support this + pane_id_to_replace, close_replaced_pane, tab_id, }]) @@ -3575,6 +3590,7 @@ mod tests { floating: false, in_place: false, close_replaced_pane: false, + pane_id: None, name: None, close_on_exit: false, start_suspended: false, @@ -3617,6 +3633,7 @@ mod tests { floating: false, in_place: false, close_replaced_pane: false, + pane_id: None, name: None, close_on_exit: false, start_suspended: false, @@ -3650,6 +3667,87 @@ mod tests { } #[test] + fn test_new_in_place_pane_with_pane_id_to_replace() { + let cli_action = CliAction::NewPane { + direction: None, + command: vec![], + plugin: None, + cwd: None, + floating: false, + in_place: true, + close_replaced_pane: true, + pane_id: Some("terminal_4".to_string()), + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: None, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_ok()); + let actions = result.unwrap(); + assert_eq!(actions.len(), 1); + match &actions[0] { + Action::NewInPlacePane { + pane_id_to_replace, .. + } => { + assert_eq!(*pane_id_to_replace, Some(PaneId::Terminal(4))); + }, + _ => panic!("Expected NewInPlacePane action"), + } + } + + #[test] + fn test_new_in_place_pane_with_malformed_pane_id() { + let cli_action = CliAction::NewPane { + direction: None, + command: vec![], + plugin: None, + cwd: None, + floating: false, + in_place: true, + close_replaced_pane: false, + pane_id: Some("not_a_pane".to_string()), + name: None, + close_on_exit: false, + start_suspended: false, + configuration: None, + skip_plugin_cache: false, + x: None, + y: None, + width: None, + height: None, + pinned: None, + stacked: false, + blocking: false, + block_until_exit_success: false, + block_until_exit_failure: false, + block_until_exit: false, + unblock_condition: None, + near_current_pane: false, + borderless: None, + tab_id: None, + }; + let result = Action::actions_from_cli(cli_action, Box::new(|| PathBuf::from("/tmp")), None); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Malformed pane id")); + } + + #[test] fn test_new_pane_floating_with_tab_id() { let cli_action = CliAction::NewPane { direction: None, @@ -3659,6 +3757,7 @@ mod tests { floating: true, in_place: false, close_replaced_pane: false, + pane_id: None, name: None, close_on_exit: false, start_suspended: false, @@ -3701,6 +3800,7 @@ mod tests { floating: false, in_place: false, close_replaced_pane: false, + pane_id: None, name: None, close_on_exit: false, start_suspended: false, @@ -3743,6 +3843,7 @@ mod tests { floating: false, in_place: false, close_replaced_pane: false, + pane_id: None, name: None, close_on_exit: false, start_suspended: false, @@ -3847,6 +3948,7 @@ mod tests { floating: false, in_place: false, close_replaced_pane: false, + pane_id: None, name: None, close_on_exit: false, start_suspended: false, @@ -3889,6 +3991,7 @@ mod tests { floating: true, in_place: false, close_replaced_pane: false, + pane_id: None, name: None, close_on_exit: false, start_suspended: false, diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index c103e580..0934287e 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -54,6 +54,7 @@ mod not_wasm { web_server_ip: None, web_server_port: None, web_server_capability: None, + pane_frame_style: None, } } diff --git a/zellij-utils/src/input/options.rs b/zellij-utils/src/input/options.rs index 8f91cb47..a1cfdd67 100644 --- a/zellij-utils/src/input/options.rs +++ b/zellij-utils/src/input/options.rs @@ -93,6 +93,56 @@ impl FromStr for OnForceClose { } } +#[derive(ArgEnum, Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum PaneFrameStyle { + Full, + Titles, + None, +} + +impl Default for PaneFrameStyle { + fn default() -> Self { + PaneFrameStyle::Titles + } +} + +impl PaneFrameStyle { + pub fn draws_full_frames(&self) -> bool { + matches!(self, PaneFrameStyle::Full) + } + + pub fn draws_titles(&self) -> bool { + matches!(self, PaneFrameStyle::Titles) + } + + pub fn from_options(options: &Options) -> Self { + if options.pane_frames == Some(false) { + return PaneFrameStyle::None; + } + match options.pane_frame_style { + Some(PaneFrameStyle::Full) => PaneFrameStyle::Full, + _ => PaneFrameStyle::Titles, + } + } +} + +impl FromStr for PaneFrameStyle { + type Err = Box<dyn std::error::Error>; + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s.trim().to_lowercase().as_str() { + "full" => Ok(PaneFrameStyle::Full), + "titles" => Ok(PaneFrameStyle::Titles), + "none" => Ok(PaneFrameStyle::None), + e => Err(format!( + "Unknown pane frame style: '{}' (expected 'full', 'titles' or 'none')", + e + ) + .into()), + } + } +} + #[derive(Clone, Default, Debug, PartialEq, Deserialize, Serialize, Args)] /// Options that can be set either through the config file, /// or cli flags - cli flags should take precedence over the config file @@ -146,6 +196,9 @@ pub struct Options { #[serde(default)] /// Set display of the pane frames (true or false) pub pane_frames: Option<bool>, + #[clap(long, arg_enum, hide_possible_values = true, value_parser)] + #[serde(default)] + pub pane_frame_style: Option<PaneFrameStyle>, #[clap(long, value_parser)] #[serde(default)] /// Mirror session when multiple users are connected (true or false) @@ -393,6 +446,7 @@ impl Options { pub fn merge(&self, other: Options) -> Options { let mouse_mode = other.mouse_mode.or(self.mouse_mode); let pane_frames = other.pane_frames.or(self.pane_frames); + let pane_frame_style = other.pane_frame_style.or(self.pane_frame_style); let auto_layout = other.auto_layout.or(self.auto_layout); let mirror_session = other.mirror_session.or(self.mirror_session); let simplified_ui = other.simplified_ui.or(self.simplified_ui); @@ -475,6 +529,7 @@ impl Options { theme_dir, mouse_mode, pane_frames, + pane_frame_style, mirror_session, on_force_close, scroll_buffer_size, @@ -534,6 +589,7 @@ impl Options { let simplified_ui = merge_bool(other.simplified_ui, self.simplified_ui); let mouse_mode = merge_bool(other.mouse_mode, self.mouse_mode); let pane_frames = merge_bool(other.pane_frames, self.pane_frames); + let pane_frame_style = other.pane_frame_style.or(self.pane_frame_style); let auto_layout = merge_bool(other.auto_layout, self.auto_layout); let mirror_session = merge_bool(other.mirror_session, self.mirror_session); let session_serialization = @@ -616,6 +672,7 @@ impl Options { theme_dir, mouse_mode, pane_frames, + pane_frame_style, mirror_session, on_force_close, scroll_buffer_size, @@ -675,6 +732,27 @@ mod tests { const TALL_NARROW: (usize, usize) = (40, 200); const LARGE: (usize, usize) = (200, 200); + #[test] + fn pane_frame_style_from_str_accepts_all_variants() { + assert_eq!( + "full".parse::<PaneFrameStyle>().unwrap(), + PaneFrameStyle::Full + ); + assert_eq!( + "titles".parse::<PaneFrameStyle>().unwrap(), + PaneFrameStyle::Titles + ); + assert_eq!( + "none".parse::<PaneFrameStyle>().unwrap(), + PaneFrameStyle::None + ); + assert_eq!( + "NONE".parse::<PaneFrameStyle>().unwrap(), + PaneFrameStyle::None + ); + assert!("bogus".parse::<PaneFrameStyle>().is_err()); + } + fn route( layout: MobileLayoutConfiguration, is_web: bool, diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs index c4941676..f1ad8118 100644 --- a/zellij-utils/src/ipc/protobuf_conversion.rs +++ b/zellij-utils/src/ipc/protobuf_conversion.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use crate::{ client_server_contract::client_server_contract::{ client_to_server_msg, server_to_client_msg, ActionMsg, AttachClientMsg, @@ -766,6 +768,11 @@ impl From<crate::input::options::Options> }), mobile_threshold_cols: options.mobile_threshold_cols.map(|v| v as u32), mobile_threshold_rows: options.mobile_threshold_rows.map(|v| v as u32), + pane_frame_style: options.pane_frame_style.map(|s| match s { + crate::input::options::PaneFrameStyle::Full => "full".to_owned(), + crate::input::options::PaneFrameStyle::Titles => "titles".to_owned(), + crate::input::options::PaneFrameStyle::None => "none".to_owned(), + }), } } } @@ -798,6 +805,12 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Options> theme_dir: options.theme_dir.map(std::path::PathBuf::from), mouse_mode: options.mouse_mode, pane_frames: options.pane_frames, + pane_frame_style: options.pane_frame_style.as_deref().and_then(|s| match s { + "full" => Some(crate::input::options::PaneFrameStyle::Full), + "titles" => Some(crate::input::options::PaneFrameStyle::Titles), + "none" => Some(crate::input::options::PaneFrameStyle::None), + _ => None, + }), mirror_session: options.mirror_session, on_force_close: options .on_force_close @@ -999,6 +1012,7 @@ impl From<crate::input::actions::Action> SetLightThemeAction, SetPaneBorderlessAction, SetPaneColorAction, + SetPaneFrameStyleAction, ShowFloatingPanesAction, SkipConfirmAction, StackPanesAction, @@ -1177,6 +1191,16 @@ impl From<crate::input::actions::Action> crate::input::actions::Action::TogglePaneFrames => { ActionType::TogglePaneFrames(TogglePaneFramesAction {}) }, + crate::input::actions::Action::SetPaneFrameStyle(style) => { + let style = match style { + crate::input::options::PaneFrameStyle::Full => "full", + crate::input::options::PaneFrameStyle::Titles => "titles", + crate::input::options::PaneFrameStyle::None => "none", + }; + ActionType::SetPaneFrameStyle(SetPaneFrameStyleAction { + style: style.to_owned(), + }) + }, crate::input::actions::Action::ToggleActiveSyncTab => { ActionType::ToggleActiveSyncTab(ToggleActiveSyncTabAction {}) }, @@ -2037,6 +2061,13 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action> Ok(crate::input::actions::Action::ToggleFocusFullscreen) }, ActionType::TogglePaneFrames(_) => Ok(crate::input::actions::Action::TogglePaneFrames), + ActionType::SetPaneFrameStyle(set_pane_frame_style_action) => { + let style = crate::input::options::PaneFrameStyle::from_str( + &set_pane_frame_style_action.style, + ) + .map_err(|e| anyhow!("{}", e))?; + Ok(crate::input::actions::Action::SetPaneFrameStyle(style)) + }, ActionType::ToggleActiveSyncTab(_) => { Ok(crate::input::actions::Action::ToggleActiveSyncTab) }, diff --git a/zellij-utils/src/ipc/tests/roundtrip_tests.rs b/zellij-utils/src/ipc/tests/roundtrip_tests.rs index 280805ab..8ea01a5f 100644 --- a/zellij-utils/src/ipc/tests/roundtrip_tests.rs +++ b/zellij-utils/src/ipc/tests/roundtrip_tests.rs @@ -13,7 +13,9 @@ use crate::input::layout::{ TiledPaneLayout, }; use crate::input::mouse::{MouseEvent, MouseEventType}; -use crate::input::options::{Clipboard, MobileLayoutConfiguration, OnForceClose, Options}; +use crate::input::options::{ + Clipboard, MobileLayoutConfiguration, OnForceClose, Options, PaneFrameStyle, +}; use crate::ipc::{ ClientToServerMsg, ColorRegister, ExitReason, PaneReference, PixelDimensions, ResizeCause, ServerToClientMsg, @@ -444,6 +446,7 @@ fn test_client_messages() { theme_dir: Some(PathBuf::from("theme_dir")), mouse_mode: Some(true), pane_frames: Some(true), + pane_frame_style: Some(PaneFrameStyle::Full), mirror_session: Some(true), on_force_close: Some(OnForceClose::Quit), scroll_buffer_size: Some(100000), @@ -3536,6 +3539,24 @@ fn test_client_messages() { client_id: Some(100), is_cli_client: true, }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::SetPaneFrameStyle(PaneFrameStyle::Full), + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::SetPaneFrameStyle(PaneFrameStyle::Titles), + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); + test_client_roundtrip!(ClientToServerMsg::Action { + action: Action::SetPaneFrameStyle(PaneFrameStyle::None), + terminal_id: Some(1), + client_id: Some(100), + is_cli_client: true, + }); test_client_roundtrip!(ClientToServerMsg::ForwardedReplyFromHost { token: 0, reply_bytes: vec![], diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 14695f68..42ce65c5 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -12,7 +12,7 @@ use crate::input::keybinds::Keybinds; use crate::input::layout::{ Layout, PercentOrFixed, PluginUserConfiguration, RunPlugin, RunPluginOrAlias, TabLayoutInfo, }; -use crate::input::options::{Clipboard, OnForceClose, Options}; +use crate::input::options::{Clipboard, OnForceClose, Options, PaneFrameStyle}; use crate::input::permission::{GrantedPermission, PermissionCache}; use crate::input::plugins::PluginAliases; use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig}; @@ -455,6 +455,16 @@ impl Action { ) -> Result<Self, ConfigError> { match action_name { "WriteChars" => Ok(Action::WriteChars { chars: string }), + "SetPaneFrameStyle" => { + let style = PaneFrameStyle::from_str(string.as_str()).map_err(|e| { + ConfigError::new_kdl_error( + format!("{}", e), + action_node.span().offset(), + action_node.span().len(), + ) + })?; + Ok(Action::SetPaneFrameStyle(style)) + }, "SwitchToMode" => match InputMode::from_str(string.as_str()) { Ok(input_mode) => Ok(Action::SwitchToMode { input_mode }), Err(_e) => { @@ -734,6 +744,16 @@ impl Action { Action::HalfPageScrollDown => Some(KdlNode::new("HalfPageScrollDown")), Action::ToggleFocusFullscreen => Some(KdlNode::new("ToggleFocusFullscreen")), Action::TogglePaneFrames => Some(KdlNode::new("TogglePaneFrames")), + Action::SetPaneFrameStyle(style) => { + let mut node = KdlNode::new("SetPaneFrameStyle"); + let style = match style { + PaneFrameStyle::Full => "full", + PaneFrameStyle::Titles => "titles", + PaneFrameStyle::None => "none", + }; + node.push(style); + Some(node) + }, Action::ToggleActiveSyncTab => Some(KdlNode::new("ToggleActiveSyncTab")), Action::NewPane { direction, @@ -1652,6 +1672,11 @@ impl TryFrom<(&KdlNode, &Options)> for Action { action_arguments, kdl_action ), + "SetPaneFrameStyle" => parse_kdl_action_char_or_string_arguments!( + action_name, + action_arguments, + kdl_action + ), "Search" => parse_kdl_action_char_or_string_arguments!( action_name, action_arguments, @@ -2683,6 +2708,16 @@ impl Options { .map(|(string, _entry)| PathBuf::from(string)); let pane_frames = kdl_property_first_arg_as_bool_or_error!(kdl_options, "pane_frames").map(|(v, _)| v); + let pane_frame_style = + match kdl_property_first_arg_as_string_or_error!(kdl_options, "pane_frame_style") { + Some((string, entry)) => Some(PaneFrameStyle::from_str(string).map_err(|_| { + kdl_parsing_error!( + format!("Invalid value for pane_frame_style: '{}'", string), + entry + ) + })?), + None => None, + }; let auto_layout = kdl_property_first_arg_as_bool_or_error!(kdl_options, "auto_layout").map(|(v, _)| v); let theme = kdl_property_first_arg_as_string_or_error!(kdl_options, "theme") @@ -2881,6 +2916,7 @@ impl Options { theme_dir, mouse_mode, pane_frames, + pane_frame_style, mirror_session, on_force_close, scroll_buffer_size, @@ -3295,6 +3331,44 @@ impl Options { None } } + fn pane_frame_style_to_kdl(&self, add_comments: bool) -> Option<KdlNode> { + let comment_text = format!( + "{}\n{}\n{}\n{}\n{}\n{}", + " ", + "// Set the pane frame style when pane_frames is enabled", + "// Options:", + "// - full", + "// - titles (default)", + "// ", + ); + + let style_as_str = |style: &PaneFrameStyle| -> &'static str { + match style { + PaneFrameStyle::Full => "full", + PaneFrameStyle::Titles => "titles", + PaneFrameStyle::None => "none", + } + }; + + let create_node = |node_value: &str| -> KdlNode { + let mut node = KdlNode::new("pane_frame_style"); + node.push(node_value.to_owned()); + node + }; + if let Some(pane_frame_style) = &self.pane_frame_style { + let mut node = create_node(style_as_str(pane_frame_style)); + if add_comments { + node.set_leading(format!("{}\n", comment_text)); + } + Some(node) + } else if add_comments { + let mut node = create_node("titles"); + node.set_leading(format!("{}\n// ", comment_text)); + Some(node) + } else { + None + } + } fn mirror_session_to_kdl(&self, add_comments: bool) -> Option<KdlNode> { let comment_text = format!( "{}\n{}\n{}\n{}\n{}\n{}\n{}", @@ -4419,6 +4493,9 @@ impl Options { if let Some(pane_frames) = self.pane_frames_to_kdl(add_comments) { nodes.push(pane_frames); } + if let Some(pane_frame_style) = self.pane_frame_style_to_kdl(add_comments) { + nodes.push(pane_frame_style); + } if let Some(mirror_session) = self.mirror_session_to_kdl(add_comments) { nodes.push(mirror_session); } diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap index 5dbede33..2d0063ce 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__bare_config_from_default_assets_to_string_with_comments.snap @@ -359,6 +359,13 @@ web_client { // // pane_frames false +// Set the pane frame style when pane_frames is enabled +// Options: +// - full +// - titles (default) +// +// pane_frame_style "titles" + // When attaching to an existing session with other users, // should the session be mirrored (true) // or should each user have their own cursor (false) diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap index 347a2074..a69641fe 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__config_options_to_string_with_comments.snap @@ -77,6 +77,13 @@ mouse_mode false // pane_frames false +// Set the pane frame style when pane_frames is enabled +// Options: +// - full +// - titles (default) +// +// pane_frame_style "titles" + // When attaching to an existing session with other users, // should the session be mirrored (true) // or should each user have their own cursor (false) diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index 68c859b8..f9370961 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -2,6 +2,7 @@ syntax = "proto3"; import "input_mode.proto"; import "resize.proto"; +import "pane_frame_style.proto"; package api.action; @@ -245,9 +246,14 @@ message Action { ShowFloatingPanesPayload show_floating_panes_payload = 58; HideFloatingPanesPayload hide_floating_panes_payload = 59; AreFloatingPanesVisiblePayload are_floating_panes_visible_payload = 60; + SetPaneFrameStylePayload set_pane_frame_style_payload = 61; } } +message SetPaneFrameStylePayload { + pane_frame_style.PaneFrameStyle pane_frame_style = 1; +} + message CliPipePayload { optional string name = 1; string payload = 2; @@ -494,6 +500,7 @@ enum ActionName { SetLightTheme = 102; ToggleTheme = 103; FocusLastPane = 104; + SetPaneFrameStyle = 105; } message Position { diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index f4e35d84..209bff3f 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -58,6 +58,7 @@ pub use super::generated_api::api::{ ScrollAtPayload, SearchDirection as ProtobufSearchDirection, SearchOption as ProtobufSearchOption, + SetPaneFrameStylePayload, ShowFloatingPanesPayload, SplitDirection as ProtobufSplitDirection, SplitSize as ProtobufSplitSize, @@ -74,6 +75,7 @@ pub use super::generated_api::api::{ WritePayload, }, input_mode::InputMode as ProtobufInputMode, + pane_frame_style::PaneFrameStyle as ProtobufPaneFrameStyle, resize::{Resize as ProtobufResize, ResizeDirection as ProtobufResizeDirection}, }; use crate::data::{ @@ -300,6 +302,17 @@ impl TryFrom<ProtobufAction> for Action { Some(_) => Err("TogglePaneFrames should not have a payload"), None => Ok(Action::TogglePaneFrames), }, + Some(ProtobufActionName::SetPaneFrameStyle) => match protobuf_action.optional_payload { + Some(OptionalPayload::SetPaneFrameStylePayload(payload)) => { + match ProtobufPaneFrameStyle::from_i32(payload.pane_frame_style) { + Some(protobuf_pane_frame_style) => Ok(Action::SetPaneFrameStyle( + protobuf_pane_frame_style.try_into()?, + )), + None => Err("Malformed payload for Action::SetPaneFrameStyle"), + } + }, + _ => Err("Wrong payload for Action::SetPaneFrameStyle"), + }, Some(ProtobufActionName::ToggleActiveSyncTab) => { match protobuf_action.optional_payload { Some(_) => Err("ToggleActiveSyncTab should not have a payload"), @@ -1301,6 +1314,15 @@ impl TryFrom<Action> for ProtobufAction { name: ProtobufActionName::TogglePaneFrames as i32, optional_payload: None, }), + Action::SetPaneFrameStyle(pane_frame_style) => Ok(ProtobufAction { + name: ProtobufActionName::SetPaneFrameStyle as i32, + optional_payload: Some(OptionalPayload::SetPaneFrameStylePayload( + SetPaneFrameStylePayload { + pane_frame_style: ProtobufPaneFrameStyle::try_from(pane_frame_style)? + as i32, + }, + )), + }), Action::ToggleActiveSyncTab => Ok(ProtobufAction { name: ProtobufActionName::ToggleActiveSyncTab as i32, optional_payload: None, @@ -3235,3 +3257,23 @@ impl TryFrom<SwapFloatingLayout> for ProtobufSwapFloatingLayout { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::input::options::PaneFrameStyle; + + #[test] + fn set_pane_frame_style_action_protobuf_round_trip() { + for style in [ + PaneFrameStyle::Full, + PaneFrameStyle::Titles, + PaneFrameStyle::None, + ] { + let original = Action::SetPaneFrameStyle(style); + let protobuf: ProtobufAction = original.clone().try_into().expect("encode"); + let decoded: Action = protobuf.try_into().expect("decode"); + assert_eq!(original, decoded); + } + } +} diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index 2567c657..d3148edf 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -70,6 +70,8 @@ enum EventType { CommandChanged = 45; HostTerminalThemeChanged = 46; SoftKeyboardVisibilityChanged = 47; + HintText = 48; + ActivePaneScroll = 49; } message EventNameList { @@ -119,9 +121,29 @@ message Event { CommandChangedPayload command_changed_payload = 39; HostTerminalThemeChangedPayload host_terminal_theme_changed_payload = 40; SoftKeyboardVisibilityChangedPayload soft_keyboard_visibility_changed_payload = 41; + HintTextPayload hint_text_payload = 42; + ActivePaneScrollPayload active_pane_scroll_payload = 43; } } +message HintTextPayload { + map<uint32, StyledText> hint_text = 1; +} + +message ActivePaneScrollPayload { + optional uint32 position = 1; + optional uint32 length = 2; +} + +message StyledText { + string text = 1; + repeated StyledTextIndices indices = 2; +} + +message StyledTextIndices { + repeated uint32 indices = 1; +} + message HostTerminalThemeChangedPayload { HostTerminalThemeIndication mode = 1; } @@ -488,6 +510,7 @@ message ModeUpdatePayload { optional string web_server_ip = 13; optional uint32 web_server_port = 14; optional bool web_server_capability = 15; + optional PaneFrameStyle pane_frame_style = 16; } enum WebSharing { @@ -496,6 +519,12 @@ enum WebSharing { Disabled = 2; } +enum PaneFrameStyle { + Full = 0; + Titles = 1; + None = 2; +} + message InputModeKeybinds { input_mode.InputMode mode = 1; repeated KeyBind key_bind = 2; diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index 5ba106d0..190552d4 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -4,6 +4,7 @@ pub use super::generated_api::api::{ event::Payload as ProtobufEventPayload, layout_parsing_error::ErrorType as ProtobufLayoutParsingErrorType, pane_scrollback_response, ActionCompletePayload as ProtobufActionCompletePayload, + ActivePaneScrollPayload as ProtobufActivePaneScrollPayload, AvailableLayoutInfoPayload as ProtobufAvailableLayoutInfoPayload, ClientInfo as ProtobufClientInfo, ClientPaneHistory as ProtobufClientPaneHistory, ClientTabHistory as ProtobufClientTabHistory, @@ -11,6 +12,7 @@ pub use super::generated_api::api::{ CopyDestination as ProtobufCopyDestination, CwdChangedPayload as ProtobufCwdChangedPayload, Event as ProtobufEvent, EventNameList as ProtobufEventNameList, EventType as ProtobufEventType, FileMetadata as ProtobufFileMetadata, + HintTextPayload as ProtobufHintTextPayload, HostTerminalThemeChangedPayload as ProtobufHostTerminalThemeChangedPayload, HostTerminalThemeIndication as ProtobufHostTerminalThemeIndication, InputModeKeybinds as ProtobufInputModeKeybinds, KdlError as ProtobufKdlError, @@ -19,14 +21,16 @@ pub use super::generated_api::api::{ LayoutParsingError as ProtobufLayoutParsingError, LayoutWithError as ProtobufLayoutWithError, ModeUpdatePayload as ProtobufModeUpdatePayload, PaneContents as ProtobufPaneContents, PaneContentsEntry as ProtobufPaneContentsEntry, - PaneId as ProtobufPaneId, PaneInfo as ProtobufPaneInfo, - PaneManifest as ProtobufPaneManifest, PaneMetadata as ProtobufPaneMetadata, + PaneFrameStyle as ProtobufPaneFrameStyle, PaneId as ProtobufPaneId, + PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest, + PaneMetadata as ProtobufPaneMetadata, PaneRenderReportPayload as ProtobufPaneRenderReportPayload, PaneScrollbackResponse as ProtobufPaneScrollbackResponse, PaneType as ProtobufPaneType, PluginConfigurationChangedPayload as ProtobufPluginConfigurationChangedPayload, PluginInfo as ProtobufPluginInfo, ResurrectableSession as ProtobufResurrectableSession, SelectedText as ProtobufSelectedText, SessionManifest as ProtobufSessionManifest, SoftKeyboardVisibilityChangedPayload as ProtobufSoftKeyboardVisibilityChangedPayload, + StyledText as ProtobufStyledText, StyledTextIndices as ProtobufStyledTextIndices, SyntaxError as ProtobufSyntaxError, TabInfo as ProtobufTabInfo, TabMetadata as ProtobufTabMetadata, UserActionPayload as ProtobufUserActionPayload, WebServerStatusPayload as ProtobufWebServerStatusPayload, WebSharing as ProtobufWebSharing, @@ -41,8 +45,8 @@ use crate::data::{ ClientId, ClientInfo, CopyDestination, Event, EventType, FileMetadata, HostTerminalThemeMode, InputMode, KeyWithModifier, LayoutInfo, LayoutMetadata, ModeInfo, Mouse, PaneContents, PaneId, PaneInfo, PaneManifest, PaneMetadata, PaneScrollbackResponse, PermissionStatus, - PluginCapabilities, PluginInfo, SelectedText, SessionInfo, Style, TabInfo, TabMetadata, - WebServerStatus, WebSharing, + PluginCapabilities, PluginInfo, SelectedText, SessionInfo, Style, StyledText, TabInfo, + TabMetadata, WebServerStatus, WebSharing, }; use crate::errors::prelude::*; @@ -597,6 +601,28 @@ impl TryFrom<ProtobufEvent> for Event { _ => Err("Malformed payload for SoftKeyboardVisibilityChanged Event"), } }, + Some(ProtobufEventType::HintText) => match protobuf_event.payload { + Some(ProtobufEventPayload::HintTextPayload(p)) => { + let mut hint_text = BTreeMap::new(); + for (width, styled_text) in p.hint_text { + hint_text.insert(width as usize, StyledText::from(styled_text)); + } + Ok(Event::HintText(hint_text)) + }, + _ => Err("Malformed payload for HintText Event"), + }, + Some(ProtobufEventType::ActivePaneScroll) => match protobuf_event.payload { + Some(ProtobufEventPayload::ActivePaneScrollPayload(p)) => { + let scroll = match (p.position, p.length) { + (Some(position), Some(length)) => { + Some((position as usize, length as usize)) + }, + _ => None, + }; + Ok(Event::ActivePaneScroll(scroll)) + }, + _ => Err("Malformed payload for ActivePaneScroll Event"), + }, None => Err("Unknown Protobuf Event"), } } @@ -1147,6 +1173,35 @@ impl TryFrom<Event> for ProtobufEvent { )), }) }, + Event::HintText(hint_text) => { + let mut proto_hint_text = HashMap::new(); + for (width, styled_text) in hint_text { + proto_hint_text.insert(width as u32, ProtobufStyledText::from(styled_text)); + } + let payload = ProtobufHintTextPayload { + hint_text: proto_hint_text, + }; + Ok(ProtobufEvent { + name: ProtobufEventType::HintText as i32, + payload: Some(event::Payload::HintTextPayload(payload)), + }) + }, + Event::ActivePaneScroll(scroll) => { + let payload = match scroll { + Some((position, length)) => ProtobufActivePaneScrollPayload { + position: Some(position as u32), + length: Some(length as u32), + }, + None => ProtobufActivePaneScrollPayload { + position: None, + length: None, + }, + }; + Ok(ProtobufEvent { + name: ProtobufEventType::ActivePaneScroll as i32, + payload: Some(event::Payload::ActivePaneScrollPayload(payload)), + }) + }, Event::InitialKeybinds(keybinds) => { let mut protobuf_keybinds: Vec<ProtobufInputModeKeybinds> = vec![]; for (input_mode, input_mode_keybinds) in keybinds { @@ -1960,6 +2015,11 @@ impl TryFrom<ProtobufModeUpdatePayload> for ModeInfo { let web_server_capability = protobuf_mode_update_payload.web_server_capability; + let pane_frame_style = protobuf_mode_update_payload + .pane_frame_style + .and_then(|p| ProtobufPaneFrameStyle::from_i32(p)) + .map(|p| p.into()); + let mode_info = ModeInfo { mode: current_mode, keybinds, @@ -1976,6 +2036,7 @@ impl TryFrom<ProtobufModeUpdatePayload> for ModeInfo { web_server_ip, web_server_port, web_server_capability, + pane_frame_style, }; Ok(mode_info) } @@ -2000,6 +2061,10 @@ impl TryFrom<ModeInfo> for ProtobufModeUpdatePayload { let web_server_ip = mode_info.web_server_ip.map(|i| format!("{}", i)); let web_server_port = mode_info.web_server_port.map(|p| p as u32); let web_server_capability = mode_info.web_server_capability; + let pane_frame_style = mode_info.pane_frame_style.map(|p| { + let protobuf_pane_frame_style: ProtobufPaneFrameStyle = p.into(); + protobuf_pane_frame_style as i32 + }); let mut protobuf_input_mode_keybinds: Vec<ProtobufInputModeKeybinds> = vec![]; for (input_mode, input_mode_keybinds) in mode_info.keybinds { let mode: ProtobufInputMode = input_mode.try_into()?; @@ -2040,6 +2105,7 @@ impl TryFrom<ModeInfo> for ProtobufModeUpdatePayload { web_server_ip, web_server_port, web_server_capability, + pane_frame_style, }) } } @@ -2127,6 +2193,8 @@ impl TryFrom<ProtobufEventType> for EventType { ProtobufEventType::SoftKeyboardVisibilityChanged => { EventType::SoftKeyboardVisibilityChanged }, + ProtobufEventType::HintText => EventType::HintText, + ProtobufEventType::ActivePaneScroll => EventType::ActivePaneScroll, }) } } @@ -2184,10 +2252,40 @@ impl TryFrom<EventType> for ProtobufEventType { EventType::SoftKeyboardVisibilityChanged => { ProtobufEventType::SoftKeyboardVisibilityChanged }, + EventType::HintText => ProtobufEventType::HintText, + EventType::ActivePaneScroll => ProtobufEventType::ActivePaneScroll, }) } } +impl From<StyledText> for ProtobufStyledText { + fn from(styled_text: StyledText) -> Self { + ProtobufStyledText { + text: styled_text.text, + indices: styled_text + .indices + .into_iter() + .map(|inner| ProtobufStyledTextIndices { + indices: inner.into_iter().map(|i| i as u32).collect(), + }) + .collect(), + } + } +} + +impl From<ProtobufStyledText> for StyledText { + fn from(styled_text: ProtobufStyledText) -> Self { + StyledText { + text: styled_text.text, + indices: styled_text + .indices + .into_iter() + .map(|inner| inner.indices.into_iter().map(|i| i as usize).collect()) + .collect(), + } + } +} + impl From<HostTerminalThemeMode> for ProtobufHostTerminalThemeIndication { fn from(mode: HostTerminalThemeMode) -> Self { match mode { @@ -2361,6 +2459,7 @@ fn serialize_mode_update_event_with_non_default_values() { web_server_ip: IpAddr::from_str("127.0.0.1").ok(), web_server_port: Some(8082), web_server_capability: Some(true), + pane_frame_style: Some(crate::input::options::PaneFrameStyle::Titles), }); let protobuf_event: ProtobufEvent = mode_update_event.clone().try_into().unwrap(); let serialized_protobuf_event = protobuf_event.encode_to_vec(); @@ -2463,6 +2562,55 @@ fn serialize_pane_update_event() { } #[test] +fn serialize_hint_text_event() { + use prost::Message; + let hint_text_event = Event::HintText(BTreeMap::from([ + ( + 10, + StyledText { + text: "group".to_owned(), + indices: vec![vec![0]], + }, + ), + ( + 42, + StyledText { + text: "resize".to_owned(), + indices: vec![vec![0, 1], vec![3]], + }, + ), + ])); + let protobuf_event: ProtobufEvent = hint_text_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!( + hint_text_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); +} + +#[test] +fn serialize_active_pane_scroll_event() { + use prost::Message; + for active_pane_scroll_event in [ + Event::ActivePaneScroll(Some((3, 12))), + Event::ActivePaneScroll(None), + ] { + let protobuf_event: ProtobufEvent = active_pane_scroll_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!( + active_pane_scroll_event, deserialized_event, + "Event properly serialized/deserialized without change" + ); + } +} + +#[test] fn serialize_key_event() { use crate::data::BareKey; use prost::Message; @@ -2946,6 +3094,26 @@ impl Into<WebSharing> for ProtobufWebSharing { } } +impl Into<ProtobufPaneFrameStyle> for crate::input::options::PaneFrameStyle { + fn into(self) -> ProtobufPaneFrameStyle { + match self { + crate::input::options::PaneFrameStyle::Full => ProtobufPaneFrameStyle::Full, + crate::input::options::PaneFrameStyle::Titles => ProtobufPaneFrameStyle::Titles, + crate::input::options::PaneFrameStyle::None => ProtobufPaneFrameStyle::None, + } + } +} + +impl Into<crate::input::options::PaneFrameStyle> for ProtobufPaneFrameStyle { + fn into(self) -> crate::input::options::PaneFrameStyle { + match self { + ProtobufPaneFrameStyle::Full => crate::input::options::PaneFrameStyle::Full, + ProtobufPaneFrameStyle::Titles => crate::input::options::PaneFrameStyle::Titles, + ProtobufPaneFrameStyle::None => crate::input::options::PaneFrameStyle::None, + } + } +} + impl TryFrom<WebServerStatus> for ProtobufWebServerStatusPayload { type Error = &'static str; fn try_from(web_server_status: WebServerStatus) -> Result<Self, &'static str> { diff --git a/zellij-utils/src/plugin_api/mod.rs b/zellij-utils/src/plugin_api/mod.rs index 0e26485e..918ad4ae 100644 --- a/zellij-utils/src/plugin_api/mod.rs +++ b/zellij-utils/src/plugin_api/mod.rs @@ -5,6 +5,7 @@ pub mod file; pub mod input_mode; pub mod key; pub mod message; +pub mod pane_frame_style; pub mod pipe_message; pub mod plugin_command; pub mod plugin_ids; diff --git a/zellij-utils/src/plugin_api/pane_frame_style.proto b/zellij-utils/src/plugin_api/pane_frame_style.proto new file mode 100644 index 00000000..0acd8bee --- /dev/null +++ b/zellij-utils/src/plugin_api/pane_frame_style.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package api.pane_frame_style; + +enum PaneFrameStyle { + Full = 0; + Titles = 1; + None = 2; +} diff --git a/zellij-utils/src/plugin_api/pane_frame_style.rs b/zellij-utils/src/plugin_api/pane_frame_style.rs new file mode 100644 index 00000000..392f2e89 --- /dev/null +++ b/zellij-utils/src/plugin_api/pane_frame_style.rs @@ -0,0 +1,49 @@ +pub use super::generated_api::api::pane_frame_style::PaneFrameStyle as ProtobufPaneFrameStyle; +use crate::input::options::PaneFrameStyle; + +use std::convert::TryFrom; + +impl TryFrom<ProtobufPaneFrameStyle> for PaneFrameStyle { + type Error = &'static str; + fn try_from(protobuf_pane_frame_style: ProtobufPaneFrameStyle) -> Result<Self, &'static str> { + match protobuf_pane_frame_style { + ProtobufPaneFrameStyle::Full => Ok(PaneFrameStyle::Full), + ProtobufPaneFrameStyle::Titles => Ok(PaneFrameStyle::Titles), + ProtobufPaneFrameStyle::None => Ok(PaneFrameStyle::None), + } + } +} + +impl TryFrom<PaneFrameStyle> for ProtobufPaneFrameStyle { + type Error = &'static str; + fn try_from(pane_frame_style: PaneFrameStyle) -> Result<Self, &'static str> { + Ok(match pane_frame_style { + PaneFrameStyle::Full => ProtobufPaneFrameStyle::Full, + PaneFrameStyle::Titles => ProtobufPaneFrameStyle::Titles, + PaneFrameStyle::None => ProtobufPaneFrameStyle::None, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pane_frame_style_roundtrips_through_protobuf() { + for style in [ + PaneFrameStyle::Full, + PaneFrameStyle::Titles, + PaneFrameStyle::None, + ] { + let protobuf: ProtobufPaneFrameStyle = style.try_into().unwrap(); + let roundtripped: PaneFrameStyle = protobuf.try_into().unwrap(); + assert_eq!(style, roundtripped); + } + } + + #[test] + fn unknown_pane_frame_style_value_is_rejected() { + assert!(ProtobufPaneFrameStyle::from_i32(99).is_none()); + } +} diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 82f4d835..8d31727d 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -9,6 +9,7 @@ import "resize.proto"; import "plugin_permission.proto"; import "input_mode.proto"; import "key.proto"; +import "pane_frame_style.proto"; package api.plugin_command; @@ -214,6 +215,9 @@ enum CommandName { NewTiledPaneInTab = 221; ExitMobileMode = 222; FocusLastPane = 223; + SetPaneFrameStyle = 224; + ToggleFloatingPanes = 225; + NewPane = 226; } message PluginCommand { @@ -373,9 +377,19 @@ message PluginCommand { PaneId set_shadow_focus_payload = 167; NewTabUnfocusedPayload new_tab_unfocused_payload = 168; NewTiledPaneInTabPayload new_tiled_pane_in_tab_payload = 169; + SetPaneFrameStylePayload set_pane_frame_style_payload = 170; + ToggleFloatingPanesPayload toggle_floating_panes_payload = 171; } } +message SetPaneFrameStylePayload { + pane_frame_style.PaneFrameStyle pane_frame_style = 1; +} + +message ToggleFloatingPanesPayload { + optional uint64 tab_id = 1; +} + message NewTabPayload { optional string name = 1; optional string cwd = 2; diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index 546c3586..38c42565 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -6,6 +6,7 @@ pub use super::generated_api::api::{ SessionManifest as ProtobufSessionManifest, }, input_mode::InputMode as ProtobufInputMode, + pane_frame_style::PaneFrameStyle as ProtobufPaneFrameStyle, plugin_command::{ break_panes_to_new_tab_response, break_panes_to_tab_with_id_response, break_panes_to_tab_with_index_response, delete_layout_response, dump_layout_response, @@ -120,16 +121,18 @@ pub use super::generated_api::api::{ SaveSessionResponse as ProtobufSaveSessionResponse, ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SessionListSnapshot as ProtobufSessionListSnapshot, SetFloatingPanePinnedPayload, - SetPaneBorderlessPayload, SetPaneColorPayload, SetPaneRegexHighlightsPayload, - SetSelfMouseSelectionSupportPayload, + SetPaneBorderlessPayload, SetPaneColorPayload, + SetPaneFrameStylePayload as ProtobufSetPaneFrameStylePayload, + SetPaneRegexHighlightsPayload, SetSelfMouseSelectionSupportPayload, SetSoftKeyboardPayload as ProtobufSetSoftKeyboardPayload, SetTabFitPayload as ProtobufSetTabFitPayload, SetTimeoutPayload, ShowCursorPayload, ShowFloatingPanesPayload as ProtobufShowFloatingPanesPayload, ShowFloatingPanesResponse as ProtobufShowFloatingPanesResponse, ShowPaneWithIdPayload, Size as ProtobufSize, StackPanesPayload, SubscribePayload, SwitchSessionPayload, - SwitchTabToIdPayload, SwitchTabToPayload, TogglePaneBorderlessPayload, - TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, UnsubscribePayload, - WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload, + SwitchTabToIdPayload, SwitchTabToPayload, ToggleFloatingPanesPayload, + TogglePaneBorderlessPayload, TogglePaneEmbedOrEjectForPaneIdPayload, + TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload, + WriteCharsToPaneIdPayload, WriteToPaneIdPayload, }, plugin_permission::PermissionType as ProtobufPermissionType, resize::ResizeAction as ProtobufResizeAction, @@ -940,6 +943,20 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand { }, _ => Err("Mismatched payload for NewTiledPaneInTab"), }, + Some(CommandName::ToggleFloatingPanes) => match protobuf_plugin_command.payload { + Some(Payload::ToggleFloatingPanesPayload(payload)) => { + Ok(PluginCommand::ToggleFloatingPanes { + tab_id: payload.tab_id, + }) + }, + _ => Ok(PluginCommand::ToggleFloatingPanes { tab_id: None }), + }, + Some(CommandName::NewPane) => { + if protobuf_plugin_command.payload.is_some() { + return Err("NewPane should not have a payload"); + } + Ok(PluginCommand::NewPane) + }, Some(CommandName::GoToNextTab) => { if protobuf_plugin_command.payload.is_some() { return Err("GoToNextTab should not have a payload"); @@ -1099,6 +1116,17 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand { } Ok(PluginCommand::TogglePaneFrames) }, + Some(CommandName::SetPaneFrameStyle) => match protobuf_plugin_command.payload { + Some(Payload::SetPaneFrameStylePayload(payload)) => { + match ProtobufPaneFrameStyle::from_i32(payload.pane_frame_style) { + Some(protobuf_pane_frame_style) => Ok(PluginCommand::SetPaneFrameStyle( + protobuf_pane_frame_style.try_into()?, + )), + None => Err("Malformed SetPaneFrameStyle payload"), + } + }, + _ => Err("Mismatched payload for SetPaneFrameStyle"), + }, Some(CommandName::TogglePaneEmbedOrEject) => { if protobuf_plugin_command.payload.is_some() { return Err("TogglePaneEmbedOrEject should not have a payload"); @@ -2983,6 +3011,16 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { }, )), }), + PluginCommand::ToggleFloatingPanes { tab_id } => Ok(ProtobufPluginCommand { + name: CommandName::ToggleFloatingPanes as i32, + payload: Some(Payload::ToggleFloatingPanesPayload( + ToggleFloatingPanesPayload { tab_id }, + )), + }), + PluginCommand::NewPane => Ok(ProtobufPluginCommand { + name: CommandName::NewPane as i32, + payload: None, + }), PluginCommand::GoToNextTab => Ok(ProtobufPluginCommand { name: CommandName::GoToNextTab as i32, payload: None, @@ -3093,6 +3131,15 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand { name: CommandName::TogglePaneFrames as i32, payload: None, }), + PluginCommand::SetPaneFrameStyle(pane_frame_style) => Ok(ProtobufPluginCommand { + name: CommandName::SetPaneFrameStyle as i32, + payload: Some(Payload::SetPaneFrameStylePayload( + ProtobufSetPaneFrameStylePayload { + pane_frame_style: ProtobufPaneFrameStyle::try_from(pane_frame_style)? + as i32, + }, + )), + }), PluginCommand::TogglePaneEmbedOrEject => Ok(ProtobufPluginCommand { name: CommandName::TogglePaneEmbedOrEject as i32, payload: None, @@ -5265,4 +5312,57 @@ mod tests { other => panic!("expected SetTabFit, got {:?}", other), } } + + #[test] + fn set_pane_frame_style_protobuf_round_trip() { + use crate::input::options::PaneFrameStyle; + for style in [ + PaneFrameStyle::Full, + PaneFrameStyle::Titles, + PaneFrameStyle::None, + ] { + let original = PluginCommand::SetPaneFrameStyle(style); + let protobuf: ProtobufPluginCommand = original.try_into().expect("encode"); + let decoded: PluginCommand = protobuf.try_into().expect("decode"); + match decoded { + PluginCommand::SetPaneFrameStyle(decoded_style) => { + assert_eq!(decoded_style, style) + }, + other => panic!("expected SetPaneFrameStyle, got {:?}", other), + } + } + } + + #[test] + fn new_pane_protobuf_round_trip() { + let original = PluginCommand::NewPane; + let protobuf: ProtobufPluginCommand = original.try_into().expect("encode"); + let decoded: PluginCommand = protobuf.try_into().expect("decode"); + match decoded { + PluginCommand::NewPane => {}, + other => panic!("expected NewPane, got {:?}", other), + } + } + + #[test] + fn toggle_floating_panes_focused_tab_protobuf_round_trip() { + let original = PluginCommand::ToggleFloatingPanes { tab_id: None }; + let protobuf: ProtobufPluginCommand = original.try_into().expect("encode"); + let decoded: PluginCommand = protobuf.try_into().expect("decode"); + match decoded { + PluginCommand::ToggleFloatingPanes { tab_id } => assert_eq!(tab_id, None), + other => panic!("expected ToggleFloatingPanes, got {:?}", other), + } + } + + #[test] + fn toggle_floating_panes_with_tab_id_protobuf_round_trip() { + let original = PluginCommand::ToggleFloatingPanes { tab_id: Some(2) }; + let protobuf: ProtobufPluginCommand = original.try_into().expect("encode"); + let decoded: PluginCommand = protobuf.try_into().expect("decode"); + match decoded { + PluginCommand::ToggleFloatingPanes { tab_id } => assert_eq!(tab_id, Some(2)), + other => panic!("expected ToggleFloatingPanes, got {:?}", other), + } + } } diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap index 0d51e89b..4d16821f 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_config_options.snap @@ -17,6 +17,7 @@ Options { theme_dir: None, mouse_mode: None, pane_frames: None, + pane_frame_style: None, mirror_session: None, on_force_close: None, scroll_buffer_size: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap index 1b0c154a..d8378848 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__cli_arguments_override_layout_options.snap @@ -17,6 +17,7 @@ Options { pane_frames: Some( true, ), + pane_frame_style: None, mirror_session: None, on_force_close: None, scroll_buffer_size: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap index 08b89aac..79a656a5 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments-3.snap @@ -15,6 +15,7 @@ Options { theme_dir: None, mouse_mode: None, pane_frames: None, + pane_frame_style: None, mirror_session: None, on_force_close: None, scroll_buffer_size: None, 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 a8a144a8..2f8c1964 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 @@ -5993,6 +5993,7 @@ Config { theme_dir: None, mouse_mode: None, pane_frames: None, + pane_frame_style: None, mirror_session: None, on_force_close: None, scroll_buffer_size: None, 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 4792ab0c..e4a22a82 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 @@ -5993,6 +5993,7 @@ Config { theme_dir: None, mouse_mode: None, pane_frames: None, + pane_frame_style: None, mirror_session: None, on_force_close: None, scroll_buffer_size: None, 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 8f5baed6..f703371c 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 @@ -102,6 +102,7 @@ Config { theme_dir: None, mouse_mode: None, pane_frames: None, + pane_frame_style: None, mirror_session: None, on_force_close: None, scroll_buffer_size: None, diff --git a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap index 40822e6e..0830a4b6 100644 --- a/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap +++ b/zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_options_override_config_options.snap @@ -17,6 +17,7 @@ Options { pane_frames: Some( false, ), + pane_frame_style: None, mirror_session: None, on_force_close: None, scroll_buffer_size: None, 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 2cb1c9af..e8e1b3ee 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 @@ -5993,6 +5993,7 @@ Config { theme_dir: None, mouse_mode: None, pane_frames: None, + pane_frame_style: None, mirror_session: None, on_force_close: None, scroll_buffer_size: None, 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 431c8fc4..ff3a4fda 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 @@ -5993,6 +5993,7 @@ Config { theme_dir: None, mouse_mode: None, pane_frames: None, + pane_frame_style: None, mirror_session: None, on_force_close: None, scroll_buffer_size: None, |
