aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-06-26 13:51:17 +0200
committerGitHub <noreply@github.com>2025-06-26 13:51:17 +0200
commit11015c8fe4db1a8ffe77e0c569f0e026c53d9776 (patch)
tree82c8513128e4be58cf6ac991b9872c9b44be5b46
parentdocs(changelog): terminal performance (diff)
downloadsl0ppij-11015c8fe4db1a8ffe77e0c569f0e026c53d9776.tar.gz
sl0ppij-11015c8fe4db1a8ffe77e0c569f0e026c53d9776.tar.xz
sl0ppij-11015c8fe4db1a8ffe77e0c569f0e026c53d9776.zip
feat(plugin-api): allow replacing pane with existing pane (#4246)
* work * make the api work * some cleanups * close pane * style(fmt): rustfmt
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs22
-rw-r--r--zellij-server/src/screen.rs51
-rw-r--r--zellij-server/src/tab/mod.rs24
-rw-r--r--zellij-tile/src/shim.rs8
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs15
-rw-r--r--zellij-utils/src/data.rs1
-rw-r--r--zellij-utils/src/errors.rs1
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto7
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs45
9 files changed, 164 insertions, 10 deletions
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<dyn Pane>,
+ ) {
+ 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<plugin_command::Payload>,
}
/// 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<PaneId>,
+ #[prost(message, optional, tag="2")]
+ pub existing_pane_id: ::core::option::Option<PaneId>,
+}
+#[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<ProtobufPluginCommand> 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<PluginCommand> 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(),
+ },
+ )),
+ })
+ },
}
}
}