feat(plugin-api): allow replacing pane with existing pane (#4246)

* work

* make the api work

* some cleanups

* close pane

* style(fmt): rustfmt
This commit is contained in:
Aram Drevekenin 2025-06-26 13:51:17 +02:00 committed by GitHub
parent 16dd0a91cd
commit 11015c8fe4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 164 additions and 10 deletions

View file

@ -495,6 +495,14 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
PluginCommand::ClearKeyPressesIntercepts => { PluginCommand::ClearKeyPressesIntercepts => {
clear_key_presses_intercepts(&mut env) 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) => { (PermissionStatus::Denied, permission) => {
log::error!( log::error!(
@ -2455,6 +2463,19 @@ fn clear_key_presses_intercepts(env: &mut PluginEnv) {
.send_to_screen(ScreenInstruction::ClearKeyPressesIntercepts(env.client_id)); .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. // Custom panic handler for plugins.
// //
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the // 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::CloseMultiplePanes(..)
| PluginCommand::FloatMultiplePanes(..) | PluginCommand::FloatMultiplePanes(..)
| PluginCommand::EmbedMultiplePanes(..) | PluginCommand::EmbedMultiplePanes(..)
| PluginCommand::ReplacePaneWithExistingPane(..)
| PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState, | PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState,
PluginCommand::UnblockCliPipeInput(..) PluginCommand::UnblockCliPipeInput(..)
| PluginCommand::BlockCliPipeInput(..) | PluginCommand::BlockCliPipeInput(..)

View file

@ -422,6 +422,7 @@ pub enum ScreenInstruction {
SetMouseSelectionSupport(PaneId, bool), SetMouseSelectionSupport(PaneId, bool),
InterceptKeyPresses(PluginId, ClientId), InterceptKeyPresses(PluginId, ClientId),
ClearKeyPressesIntercepts(ClientId), ClearKeyPressesIntercepts(ClientId),
ReplacePaneWithExistingPane(PaneId, PaneId),
} }
impl From<&ScreenInstruction> for ScreenContext { impl From<&ScreenInstruction> for ScreenContext {
@ -654,6 +655,9 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::ClearKeyPressesIntercepts(..) => { ScreenInstruction::ClearKeyPressesIntercepts(..) => {
ScreenContext::ClearKeyPressesIntercepts ScreenContext::ClearKeyPressesIntercepts
}, },
ScreenInstruction::ReplacePaneWithExistingPane(..) => {
ScreenContext::ReplacePaneWithExistingPane
},
} }
} }
} }
@ -2608,6 +2612,50 @@ impl Screen {
} }
Ok(()) 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( pub fn reconfigure(
&mut self, &mut self,
new_keybinds: Keybinds, new_keybinds: Keybinds,
@ -5488,6 +5536,9 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::ClearKeyPressesIntercepts(client_id) => { ScreenInstruction::ClearKeyPressesIntercepts(client_id) => {
keybind_intercepts.remove(&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(()) Ok(())

View file

@ -1617,6 +1617,30 @@ impl Tab {
} }
Ok(()) 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( pub fn horizontal_split(
&mut self, &mut self,
pid: PaneId, pid: PaneId,

View file

@ -1451,6 +1451,14 @@ pub fn clear_key_presses_intercepts() {
unsafe { host_run_plugin_command() }; 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 // Utility Functions
#[allow(unused)] #[allow(unused)]

View file

@ -3,7 +3,7 @@
pub struct PluginCommand { pub struct PluginCommand {
#[prost(enumeration="CommandName", tag="1")] #[prost(enumeration="CommandName", tag="1")]
pub name: i32, 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>, pub payload: ::core::option::Option<plugin_command::Payload>,
} }
/// Nested message and enum types in `PluginCommand`. /// Nested message and enum types in `PluginCommand`.
@ -211,10 +211,20 @@ pub mod plugin_command {
RevokeWebLoginTokenPayload(super::RevokeWebLoginTokenPayload), RevokeWebLoginTokenPayload(super::RevokeWebLoginTokenPayload),
#[prost(message, tag="110")] #[prost(message, tag="110")]
RenameWebLoginTokenPayload(super::RenameWebLoginTokenPayload), RenameWebLoginTokenPayload(super::RenameWebLoginTokenPayload),
#[prost(message, tag="111")]
ReplacePaneWithExistingPanePayload(super::ReplacePaneWithExistingPanePayload),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[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 { pub struct RenameWebLoginTokenPayload {
#[prost(string, tag="1")] #[prost(string, tag="1")]
pub old_name: ::prost::alloc::string::String, pub old_name: ::prost::alloc::string::String,
@ -1013,6 +1023,7 @@ pub enum CommandName {
RenameWebLoginToken = 142, RenameWebLoginToken = 142,
InterceptKeyPresses = 143, InterceptKeyPresses = 143,
ClearKeyPressesIntercepts = 144, ClearKeyPressesIntercepts = 144,
ReplacePaneWithExistingPane = 155,
} }
impl CommandName { impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -1166,6 +1177,7 @@ impl CommandName {
CommandName::RenameWebLoginToken => "RenameWebLoginToken", CommandName::RenameWebLoginToken => "RenameWebLoginToken",
CommandName::InterceptKeyPresses => "InterceptKeyPresses", CommandName::InterceptKeyPresses => "InterceptKeyPresses",
CommandName::ClearKeyPressesIntercepts => "ClearKeyPressesIntercepts", CommandName::ClearKeyPressesIntercepts => "ClearKeyPressesIntercepts",
CommandName::ReplacePaneWithExistingPane => "ReplacePaneWithExistingPane",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -1316,6 +1328,7 @@ impl CommandName {
"RenameWebLoginToken" => Some(Self::RenameWebLoginToken), "RenameWebLoginToken" => Some(Self::RenameWebLoginToken),
"InterceptKeyPresses" => Some(Self::InterceptKeyPresses), "InterceptKeyPresses" => Some(Self::InterceptKeyPresses),
"ClearKeyPressesIntercepts" => Some(Self::ClearKeyPressesIntercepts), "ClearKeyPressesIntercepts" => Some(Self::ClearKeyPressesIntercepts),
"ReplacePaneWithExistingPane" => Some(Self::ReplacePaneWithExistingPane),
_ => None, _ => None,
} }
} }

View file

@ -2496,4 +2496,5 @@ pub enum PluginCommand {
RenameWebLoginToken(String, String), // (original_name, new_name) RenameWebLoginToken(String, String), // (original_name, new_name)
InterceptKeyPresses, InterceptKeyPresses,
ClearKeyPressesIntercepts, ClearKeyPressesIntercepts,
ReplacePaneWithExistingPane(PaneId, PaneId), // (pane id to replace, pane id of existing)
} }

View file

@ -387,6 +387,7 @@ pub enum ScreenContext {
SetMouseSelectionSupport, SetMouseSelectionSupport,
InterceptKeyPresses, InterceptKeyPresses,
ClearKeyPressesIntercepts, ClearKeyPressesIntercepts,
ReplacePaneWithExistingPane,
} }
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s. /// Stack call representations corresponding to the different types of [`PtyInstruction`]s.

View file

@ -158,6 +158,7 @@ enum CommandName {
RenameWebLoginToken = 142; RenameWebLoginToken = 142;
InterceptKeyPresses = 143; InterceptKeyPresses = 143;
ClearKeyPressesIntercepts = 144; ClearKeyPressesIntercepts = 144;
ReplacePaneWithExistingPane = 155;
} }
message PluginCommand { message PluginCommand {
@ -263,9 +264,15 @@ message PluginCommand {
GenerateWebLoginTokenPayload generate_web_login_token_payload = 108; GenerateWebLoginTokenPayload generate_web_login_token_payload = 108;
RevokeWebLoginTokenPayload revoke_web_login_token_payload = 109; RevokeWebLoginTokenPayload revoke_web_login_token_payload = 109;
RenameWebLoginTokenPayload rename_web_login_token_payload = 110; 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 { message RenameWebLoginTokenPayload {
string old_name = 1; string old_name = 1;
string new_name = 2; string new_name = 2;

View file

@ -26,15 +26,15 @@ pub use super::generated_api::api::{
PaneIdAndFloatingPaneCoordinates, PaneType as ProtobufPaneType, PaneIdAndFloatingPaneCoordinates, PaneType as ProtobufPaneType,
PluginCommand as ProtobufPluginCommand, PluginMessagePayload, RebindKeysPayload, PluginCommand as ProtobufPluginCommand, PluginMessagePayload, RebindKeysPayload,
ReconfigurePayload, ReloadPluginPayload, RenameWebLoginTokenPayload, ReconfigurePayload, ReloadPluginPayload, RenameWebLoginTokenPayload,
RenameWebTokenResponse, RequestPluginPermissionPayload, RerunCommandPanePayload, RenameWebTokenResponse, ReplacePaneWithExistingPanePayload, RequestPluginPermissionPayload,
ResizePaneIdWithDirectionPayload, ResizePayload, RevokeAllWebTokensResponse, RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload,
RevokeTokenResponse, RevokeWebLoginTokenPayload, RunCommandPayload, RevokeAllWebTokensResponse, RevokeTokenResponse, RevokeWebLoginTokenPayload,
ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, RunCommandPayload, ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload,
ScrollUpInPaneIdPayload, SetFloatingPanePinnedPayload, SetSelfMouseSelectionSupportPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SetFloatingPanePinnedPayload,
SetTimeoutPayload, ShowPaneWithIdPayload, StackPanesPayload, SubscribePayload, SetSelfMouseSelectionSupportPayload, SetTimeoutPayload, ShowPaneWithIdPayload,
SwitchSessionPayload, SwitchTabToPayload, TogglePaneEmbedOrEjectForPaneIdPayload, StackPanesPayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload,
TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload, TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, UnsubscribePayload,
WriteCharsToPaneIdPayload, WriteToPaneIdPayload, WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload,
}, },
plugin_permission::PermissionType as ProtobufPermissionType, plugin_permission::PermissionType as ProtobufPermissionType,
resize::ResizeAction as ProtobufResizeAction, resize::ResizeAction as ProtobufResizeAction,
@ -1711,6 +1711,22 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
Some(_) => Err("ClearKeyPressesIntercepts should have no payload, found a payload"), Some(_) => Err("ClearKeyPressesIntercepts should have no payload, found a payload"),
None => Ok(PluginCommand::ClearKeyPressesIntercepts), 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"), None => Err("Unrecognized plugin command"),
} }
} }
@ -2848,6 +2864,17 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
name: CommandName::ClearKeyPressesIntercepts as i32, name: CommandName::ClearKeyPressesIntercepts as i32,
payload: None, 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(),
},
)),
})
},
} }
} }
} }