diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index b928b224..7b682f6d 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -495,6 +495,14 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { PluginCommand::ClearKeyPressesIntercepts => { clear_key_presses_intercepts(&mut env) }, + PluginCommand::ReplacePaneWithExistingPane( + pane_id_to_replace, + existing_pane_id, + ) => replace_pane_with_existing_pane( + &mut env, + pane_id_to_replace.into(), + existing_pane_id.into(), + ), }, (PermissionStatus::Denied, permission) => { log::error!( @@ -2455,6 +2463,19 @@ fn clear_key_presses_intercepts(env: &mut PluginEnv) { .send_to_screen(ScreenInstruction::ClearKeyPressesIntercepts(env.client_id)); } +fn replace_pane_with_existing_pane( + env: &mut PluginEnv, + pane_to_replace: PaneId, + existing_pane: PaneId, +) { + let _ = env + .senders + .send_to_screen(ScreenInstruction::ReplacePaneWithExistingPane( + pane_to_replace, + existing_pane, + )); +} + // Custom panic handler for plugins. // // This is called when a panic occurs in a plugin. Since most panics will likely originate in the @@ -2621,6 +2642,7 @@ fn check_command_permission( | PluginCommand::CloseMultiplePanes(..) | PluginCommand::FloatMultiplePanes(..) | PluginCommand::EmbedMultiplePanes(..) + | PluginCommand::ReplacePaneWithExistingPane(..) | PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState, PluginCommand::UnblockCliPipeInput(..) | PluginCommand::BlockCliPipeInput(..) diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index d1b2139c..376c726e 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -422,6 +422,7 @@ pub enum ScreenInstruction { SetMouseSelectionSupport(PaneId, bool), InterceptKeyPresses(PluginId, ClientId), ClearKeyPressesIntercepts(ClientId), + ReplacePaneWithExistingPane(PaneId, PaneId), } impl From<&ScreenInstruction> for ScreenContext { @@ -654,6 +655,9 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ClearKeyPressesIntercepts(..) => { ScreenContext::ClearKeyPressesIntercepts }, + ScreenInstruction::ReplacePaneWithExistingPane(..) => { + ScreenContext::ReplacePaneWithExistingPane + }, } } } @@ -2608,6 +2612,50 @@ impl Screen { } Ok(()) } + pub fn replace_pane_with_existing_pane( + &mut self, + pane_id_to_replace: PaneId, + pane_id_of_existing_pane: PaneId, + ) { + let Some(tab_index_of_pane_id_to_replace) = self + .tabs + .iter() + .find(|(_tab_index, tab)| tab.has_pane_with_pid(&pane_id_to_replace)) + .map(|(_tab_index, tab)| tab.position) + else { + log::error!("Could not find tab"); + return; + }; + let Some(tab_index_of_existing_pane) = self + .tabs + .iter() + .find(|(_tab_index, tab)| tab.has_pane_with_pid(&pane_id_of_existing_pane)) + .map(|(_tab_index, tab)| tab.position) + else { + log::error!("Could not find tab"); + return; + }; + let Some(extracted_pane_from_other_tab) = self + .tabs + .iter_mut() + .find(|(_, t)| t.position == tab_index_of_existing_pane) + .and_then(|(_, t)| t.extract_pane(pane_id_of_existing_pane, false)) + else { + log::error!("Failed to find pane"); + return; + }; + if let Some(tab) = self + .tabs + .iter_mut() + .find(|(_, t)| t.position == tab_index_of_pane_id_to_replace) + { + tab.1.close_pane_and_replace_with_other_pane( + pane_id_to_replace, + extracted_pane_from_other_tab, + ); + } + let _ = self.log_and_report_session_state(); + } pub fn reconfigure( &mut self, new_keybinds: Keybinds, @@ -5488,6 +5536,9 @@ pub(crate) fn screen_thread_main( ScreenInstruction::ClearKeyPressesIntercepts(client_id) => { keybind_intercepts.remove(&client_id); }, + ScreenInstruction::ReplacePaneWithExistingPane(old_pane_id, new_pane_id) => { + screen.replace_pane_with_existing_pane(old_pane_id, new_pane_id) + }, } } Ok(()) diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 0e4ee7fe..683a3d4a 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -1617,6 +1617,30 @@ impl Tab { } Ok(()) } + pub fn close_pane_and_replace_with_other_pane( + &mut self, + pane_id_to_replace: PaneId, + pane_to_replace_with: Box, + ) { + let mut replaced_pane = if self.floating_panes.panes_contain(&pane_id_to_replace) { + self.floating_panes + .replace_pane(pane_id_to_replace, pane_to_replace_with) + .ok() + } else { + self.tiled_panes + .replace_pane(pane_id_to_replace, pane_to_replace_with) + }; + if let Some(replaced_pane) = replaced_pane.take() { + let pane_id = replaced_pane.pid(); + let _ = self.senders.send_to_pty(PtyInstruction::ClosePane(pane_id)); + let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![( + None, + None, + Event::PaneClosed(pane_id.into()), + )])); + drop(replaced_pane); + } + } pub fn horizontal_split( &mut self, pid: PaneId, diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 4a0945ea..608fa2e7 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -1451,6 +1451,14 @@ pub fn clear_key_presses_intercepts() { unsafe { host_run_plugin_command() }; } +pub fn replace_pane_with_existing_pane(pane_id_to_replace: PaneId, existing_pane_id: PaneId) { + let plugin_command = + PluginCommand::ReplacePaneWithExistingPane(pane_id_to_replace, existing_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() }; +} + // Utility Functions #[allow(unused)] diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index 52a8cd42..e3c63626 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -3,7 +3,7 @@ pub struct PluginCommand { #[prost(enumeration="CommandName", tag="1")] pub name: i32, - #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110")] + #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111")] pub payload: ::core::option::Option, } /// Nested message and enum types in `PluginCommand`. @@ -211,10 +211,20 @@ pub mod plugin_command { RevokeWebLoginTokenPayload(super::RevokeWebLoginTokenPayload), #[prost(message, tag="110")] RenameWebLoginTokenPayload(super::RenameWebLoginTokenPayload), + #[prost(message, tag="111")] + ReplacePaneWithExistingPanePayload(super::ReplacePaneWithExistingPanePayload), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct ReplacePaneWithExistingPanePayload { + #[prost(message, optional, tag="1")] + pub pane_id_to_replace: ::core::option::Option, + #[prost(message, optional, tag="2")] + pub existing_pane_id: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct RenameWebLoginTokenPayload { #[prost(string, tag="1")] pub old_name: ::prost::alloc::string::String, @@ -1013,6 +1023,7 @@ pub enum CommandName { RenameWebLoginToken = 142, InterceptKeyPresses = 143, ClearKeyPressesIntercepts = 144, + ReplacePaneWithExistingPane = 155, } impl CommandName { /// String value of the enum field names used in the ProtoBuf definition. @@ -1166,6 +1177,7 @@ impl CommandName { CommandName::RenameWebLoginToken => "RenameWebLoginToken", CommandName::InterceptKeyPresses => "InterceptKeyPresses", CommandName::ClearKeyPressesIntercepts => "ClearKeyPressesIntercepts", + CommandName::ReplacePaneWithExistingPane => "ReplacePaneWithExistingPane", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -1316,6 +1328,7 @@ impl CommandName { "RenameWebLoginToken" => Some(Self::RenameWebLoginToken), "InterceptKeyPresses" => Some(Self::InterceptKeyPresses), "ClearKeyPressesIntercepts" => Some(Self::ClearKeyPressesIntercepts), + "ReplacePaneWithExistingPane" => Some(Self::ReplacePaneWithExistingPane), _ => None, } } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index c6b6dcad..0520e15d 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -2496,4 +2496,5 @@ pub enum PluginCommand { RenameWebLoginToken(String, String), // (original_name, new_name) InterceptKeyPresses, ClearKeyPressesIntercepts, + ReplacePaneWithExistingPane(PaneId, PaneId), // (pane id to replace, pane id of existing) } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 4d647eee..b4082cd1 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -387,6 +387,7 @@ pub enum ScreenContext { SetMouseSelectionSupport, InterceptKeyPresses, ClearKeyPressesIntercepts, + ReplacePaneWithExistingPane, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 4c1db71f..b1797214 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -158,6 +158,7 @@ enum CommandName { RenameWebLoginToken = 142; InterceptKeyPresses = 143; ClearKeyPressesIntercepts = 144; + ReplacePaneWithExistingPane = 155; } message PluginCommand { @@ -263,9 +264,15 @@ message PluginCommand { GenerateWebLoginTokenPayload generate_web_login_token_payload = 108; RevokeWebLoginTokenPayload revoke_web_login_token_payload = 109; RenameWebLoginTokenPayload rename_web_login_token_payload = 110; + ReplacePaneWithExistingPanePayload replace_pane_with_existing_pane_payload = 111; } } +message ReplacePaneWithExistingPanePayload { + PaneId pane_id_to_replace = 1; + PaneId existing_pane_id = 2; +} + message RenameWebLoginTokenPayload { string old_name = 1; string new_name = 2; diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index 7fb583b7..296a8a74 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -26,15 +26,15 @@ pub use super::generated_api::api::{ PaneIdAndFloatingPaneCoordinates, PaneType as ProtobufPaneType, PluginCommand as ProtobufPluginCommand, PluginMessagePayload, RebindKeysPayload, ReconfigurePayload, ReloadPluginPayload, RenameWebLoginTokenPayload, - RenameWebTokenResponse, RequestPluginPermissionPayload, RerunCommandPanePayload, - ResizePaneIdWithDirectionPayload, ResizePayload, RevokeAllWebTokensResponse, - RevokeTokenResponse, RevokeWebLoginTokenPayload, RunCommandPayload, - ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, - ScrollUpInPaneIdPayload, SetFloatingPanePinnedPayload, SetSelfMouseSelectionSupportPayload, - SetTimeoutPayload, ShowPaneWithIdPayload, StackPanesPayload, SubscribePayload, - SwitchSessionPayload, SwitchTabToPayload, TogglePaneEmbedOrEjectForPaneIdPayload, - TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload, - WriteCharsToPaneIdPayload, WriteToPaneIdPayload, + RenameWebTokenResponse, ReplacePaneWithExistingPanePayload, RequestPluginPermissionPayload, + RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload, + RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload, + RunCommandPayload, ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, + ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SetFloatingPanePinnedPayload, + SetSelfMouseSelectionSupportPayload, SetTimeoutPayload, ShowPaneWithIdPayload, + StackPanesPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, + TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, UnsubscribePayload, + WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload, }, plugin_permission::PermissionType as ProtobufPermissionType, resize::ResizeAction as ProtobufResizeAction, @@ -1711,6 +1711,22 @@ impl TryFrom for PluginCommand { Some(_) => Err("ClearKeyPressesIntercepts should have no payload, found a payload"), None => Ok(PluginCommand::ClearKeyPressesIntercepts), }, + Some(CommandName::ReplacePaneWithExistingPane) => match protobuf_plugin_command.payload + { + Some(Payload::ReplacePaneWithExistingPanePayload( + replace_pane_with_other_pane_payload, + )) => Ok(PluginCommand::ReplacePaneWithExistingPane( + replace_pane_with_other_pane_payload + .pane_id_to_replace + .and_then(|p_id| PaneId::try_from(p_id).ok()) + .ok_or("Failed to parse ReplacePaneWithExistingPanePayload")?, + replace_pane_with_other_pane_payload + .existing_pane_id + .and_then(|p_id| PaneId::try_from(p_id).ok()) + .ok_or("Failed to parse ReplacePaneWithExistingPanePayload")?, + )), + _ => Err("Mismatched payload for ReplacePaneWithExistingPane"), + }, None => Err("Unrecognized plugin command"), } } @@ -2848,6 +2864,17 @@ impl TryFrom for ProtobufPluginCommand { name: CommandName::ClearKeyPressesIntercepts as i32, payload: None, }), + PluginCommand::ReplacePaneWithExistingPane(pane_id_to_replace, existing_pane_id) => { + Ok(ProtobufPluginCommand { + name: CommandName::ReplacePaneWithExistingPane as i32, + payload: Some(Payload::ReplacePaneWithExistingPanePayload( + ReplacePaneWithExistingPanePayload { + pane_id_to_replace: ProtobufPaneId::try_from(pane_id_to_replace).ok(), + existing_pane_id: ProtobufPaneId::try_from(existing_pane_id).ok(), + }, + )), + }) + }, } } }