From 42adc8cd048c03d52a9f33503d75e24364adbc68 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Tue, 31 Dec 2024 12:37:23 +0100 Subject: [PATCH] feat(ux): stack panes command (#3905) * working across tabs and floating panes through the cli * finalize cli command * plugin api * style(fmt): rustfmt * fix: re-focus pane in stack if it was focused * style(fmt): rustfmt * remove outdated comment --- zellij-server/src/panes/tiled_panes/mod.rs | 21 ++++++ .../src/panes/tiled_panes/stacked_panes.rs | 30 +++++++- zellij-server/src/plugins/zellij_exports.rs | 10 +++ zellij-server/src/route.rs | 7 ++ zellij-server/src/screen.rs | 68 ++++++++++++++++++- zellij-server/src/tab/mod.rs | 51 +++++++++++++- zellij-server/src/unit/screen_tests.rs | 40 +++++++++++ ..._tests__send_cli_stack_panes_action-2.snap | 16 +++++ ..._tests__send_cli_stack_panes_action-3.snap | 6 ++ ...en_tests__send_cli_stack_panes_action.snap | 16 +++++ zellij-tile/src/shim.rs | 7 ++ .../assets/prost/api.plugin_command.rs | 13 +++- zellij-utils/src/cli.rs | 11 +++ zellij-utils/src/data.rs | 1 + zellij-utils/src/errors.rs | 1 + zellij-utils/src/input/actions.rs | 50 +++++++++++++- zellij-utils/src/plugin_api/action.rs | 1 + .../src/plugin_api/plugin_command.proto | 6 ++ zellij-utils/src/plugin_api/plugin_command.rs | 28 +++++++- 19 files changed, 373 insertions(+), 10 deletions(-) create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action-2.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action-3.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action.snap diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index f93f3156..088d1b4f 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -818,6 +818,16 @@ impl TiledPanes { }, } } + pub fn set_geom_for_pane_with_id(&mut self, pane_id: &PaneId, geom: PaneGeom) { + match self.panes.get_mut(pane_id) { + Some(pane) => { + pane.set_geom(geom); + }, + None => { + log::error!("Failed to find pane with id: {:?}", pane_id); + }, + } + } pub fn resize(&mut self, new_screen_size: Size) { // this is blocked out to appease the borrow checker { @@ -1827,6 +1837,9 @@ impl TiledPanes { } pane_infos } + pub fn pane_id_is_focused(&self, pane_id: &PaneId) -> bool { + self.active_panes.pane_id_is_focused(pane_id) + } pub fn update_pane_themes(&mut self, theme: Palette) { self.style.colors = theme; for pane in self.panes.values_mut() { @@ -1844,6 +1857,14 @@ impl TiledPanes { pane.update_rounded_corners(rounded_corners); } } + pub fn stack_panes( + &mut self, + root_pane_id: PaneId, + pane_count_in_stack: usize, + ) -> Vec { + StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide) + .new_stack(root_pane_id, pane_count_in_stack) + } } #[allow(clippy::borrowed_box)] diff --git a/zellij-server/src/panes/tiled_panes/stacked_panes.rs b/zellij-server/src/panes/tiled_panes/stacked_panes.rs index bc5057a5..400045b9 100644 --- a/zellij-server/src/panes/tiled_panes/stacked_panes.rs +++ b/zellij-server/src/panes/tiled_panes/stacked_panes.rs @@ -310,7 +310,6 @@ impl<'a> StackedPanes<'a> { adjust_stack_geoms(new_flexible_pane_geom)?; } else { if new_rows < all_stacked_pane_positions.len() { - // TODO: test this!! we don't want crashes... return Err(anyhow!("Not enough room for stacked panes")); } let rows_deficit = current_rows - new_rows; @@ -465,6 +464,35 @@ impl<'a> StackedPanes<'a> { } Err(anyhow!("Not enough room for another pane!")) } + pub fn new_stack(&mut self, root_pane_id: PaneId, pane_count_in_stack: usize) -> Vec { + let mut stacked_geoms = vec![]; + let panes = self.panes.borrow(); + let running_stack_geom = panes.get(&root_pane_id).map(|p| p.position_and_size()); + let Some(mut running_stack_geom) = running_stack_geom else { + log::error!("Pane not found"); // TODO: better error + return stacked_geoms; + }; + running_stack_geom.is_stacked = true; + let mut pane_index_in_stack = 0; + loop { + if pane_index_in_stack == pane_count_in_stack { + break; + } + let is_last_pane_in_stack = + pane_index_in_stack == pane_count_in_stack.saturating_sub(1); + let mut geom_for_pane = running_stack_geom.clone(); + if !is_last_pane_in_stack { + geom_for_pane.rows = Dimension::fixed(1); + running_stack_geom.y += 1; + running_stack_geom + .rows + .set_inner(running_stack_geom.rows.as_usize().saturating_sub(1)); + } + stacked_geoms.push(geom_for_pane); + pane_index_in_stack += 1; + } + stacked_geoms + } fn get_all_stacks(&self) -> Result>> { let err_context = || "Failed to get positions in stack"; let panes = self.panes.borrow(); diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 4cd734de..5fba8aaa 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -358,6 +358,9 @@ fn host_run_plugin_command(caller: Caller<'_, PluginEnv>) { PluginCommand::SetFloatingPanePinned(pane_id, should_be_pinned) => { set_floating_pane_pinned(env, pane_id.into(), should_be_pinned) }, + PluginCommand::StackPanes(pane_ids) => { + stack_panes(env, pane_ids.into_iter().map(|p_id| p_id.into()).collect()) + }, }, (PermissionStatus::Denied, permission) => { log::error!( @@ -1521,6 +1524,12 @@ fn set_floating_pane_pinned(env: &PluginEnv, pane_id: PaneId, should_be_pinned: }); } +fn stack_panes(env: &PluginEnv, pane_ids: Vec) { + let _ = env + .senders + .send_to_screen(ScreenInstruction::StackPanes(pane_ids)); +} + fn scan_host_folder(env: &PluginEnv, folder_to_scan: PathBuf) { if !folder_to_scan.starts_with("/host") { log::error!( @@ -1940,6 +1949,7 @@ fn check_command_permission( | PluginCommand::ReloadPlugin(..) | PluginCommand::LoadNewPlugin { .. } | PluginCommand::SetFloatingPanePinned(..) + | PluginCommand::StackPanes(..) | PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState, PluginCommand::UnblockCliPipeInput(..) | PluginCommand::BlockCliPipeInput(..) diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index d5b7825e..d611fdcd 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -974,6 +974,13 @@ pub(crate) fn route_action( .send_to_screen(ScreenInstruction::TogglePanePinned(client_id)) .with_context(err_context)?; }, + Action::StackPanes(pane_ids_to_stack) => { + senders + .send_to_screen(ScreenInstruction::StackPanes( + pane_ids_to_stack.iter().map(|p| PaneId::from(*p)).collect(), + )) + .with_context(err_context)?; + }, } Ok(should_break) } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index b4195e37..7f1e044a 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -407,6 +407,7 @@ pub enum ScreenInstruction { ListClientsToPlugin(PluginId, ClientId), TogglePanePinned(ClientId), SetFloatingPanePinned(PaneId, bool), + StackPanes(Vec), } impl From<&ScreenInstruction> for ScreenContext { @@ -621,6 +622,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::ListClientsToPlugin(..) => ScreenContext::ListClientsToPlugin, ScreenInstruction::TogglePanePinned(..) => ScreenContext::TogglePanePinned, ScreenInstruction::SetFloatingPanePinned(..) => ScreenContext::SetFloatingPanePinned, + ScreenInstruction::StackPanes(..) => ScreenContext::StackPanes, } } } @@ -2376,7 +2378,6 @@ impl Screen { pane_title: Option, client_id_tab_index_or_pane_id: ClientTabIndexOrPaneId, ) -> Result<()> { - let err_context = || format!("failed to replace pane"); let suppress_pane = |tab: &mut Tab, pane_id: PaneId, new_pane_id: PaneId| { let _ = tab.suppress_pane_and_replace_with_pid(pane_id, new_pane_id, run); if let Some(pane_title) = pane_title { @@ -2522,6 +2523,62 @@ impl Screen { ); } } + pub fn stack_panes(&mut self, mut pane_ids_to_stack: Vec) { + if pane_ids_to_stack.is_empty() { + log::error!("Got an empty list of pane_ids to stack"); + return; + } + let stack_size = pane_ids_to_stack.len(); + let root_pane_id = pane_ids_to_stack.remove(0); + let Some(root_tab_id) = self + .tabs + .iter() + .find_map(|(tab_id, tab)| { + if tab.has_pane_with_pid(&root_pane_id) { + Some(tab_id) + } else { + None + } + }) + .copied() + else { + log::error!("Failed to find tab for root_pane_id: {:?}", root_pane_id); + return; + }; + let target_tab_has_room_for_stack = self + .tabs + .get(&root_tab_id) + .map(|t| t.has_room_for_stack(root_pane_id, stack_size)) + .unwrap_or(false); + if !target_tab_has_room_for_stack { + log::error!("No room for stack with root pane id: {:?}", root_pane_id); + return; + } + + let mut panes_to_stack = vec![]; + for (tab_id, tab) in self.tabs.iter_mut() { + if tab_id == &root_tab_id { + // we do this before we extract panes so that the extraction won't trigger a + // relayout according to the next swapped tiled pane + tab.set_tiled_panes_damaged(); + } + for pane_id in &pane_ids_to_stack { + if tab.has_pane_with_pid(&pane_id) { + match tab.extract_pane(*pane_id, false) { + Some(pane) => { + panes_to_stack.push(pane); + }, + None => { + log::error!("Failed to extract pane: {:?}", pane_id); + }, + } + } + } + } + self.tabs + .get_mut(&root_tab_id) + .map(|t| t.stack_panes(root_pane_id, panes_to_stack)); + } fn unblock_input(&self) -> Result<()> { self.bus .senders @@ -3894,7 +3951,7 @@ pub(crate) fn screen_thread_main( active_tab_and_connected_client_id!( screen, client_id, - |tab: &mut Tab, client_id: ClientId| tab.previous_swap_layout(), + |tab: &mut Tab, _client_id: ClientId| tab.previous_swap_layout(), ? ); screen.render(None)?; @@ -3905,7 +3962,7 @@ pub(crate) fn screen_thread_main( active_tab_and_connected_client_id!( screen, client_id, - |tab: &mut Tab, client_id: ClientId| tab.next_swap_layout(), + |tab: &mut Tab, _client_id: ClientId| tab.next_swap_layout(), ? ); screen.render(None)?; @@ -4739,6 +4796,11 @@ pub(crate) fn screen_thread_main( ScreenInstruction::SetFloatingPanePinned(pane_id, should_be_pinned) => { screen.set_floating_pane_pinned(pane_id, should_be_pinned); }, + ScreenInstruction::StackPanes(pane_ids_to_stack) => { + screen.stack_panes(pane_ids_to_stack); + let _ = screen.unblock_input(); + let _ = screen.render(None); + }, } } Ok(()) diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 3284203c..bb3e8a7f 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -4131,7 +4131,7 @@ impl Tab { }, } } - pub fn suppress_pane(&mut self, pane_id: PaneId, client_id: Option) { + pub fn suppress_pane(&mut self, pane_id: PaneId, _client_id: Option) { // this method places a pane in the suppressed pane with its own ID - this means we'll // not take it out of there when another pane is closed (eg. like happens with the // scrollback editor), but it has to take itself out on its own (eg. a plugin using the @@ -4388,6 +4388,55 @@ impl Tab { self.set_force_render(); } } + pub fn has_room_for_stack(&self, root_pane_id: PaneId, stack_size: usize) -> bool { + if self.floating_panes.panes_contain(&root_pane_id) + || self.suppressed_panes.contains_key(&root_pane_id) + { + log::error!("Root pane of stack cannot be floating or suppressed"); + return false; + } + self.get_pane_with_id(root_pane_id) + .map(|p| p.position_and_size().rows.as_usize() >= stack_size + MIN_TERMINAL_HEIGHT) + .unwrap_or(false) + } + pub fn set_tiled_panes_damaged(&mut self) { + self.swap_layouts.set_is_tiled_damaged(); + } + pub fn stack_panes(&mut self, root_pane_id: PaneId, mut panes_to_stack: Vec>) { + if panes_to_stack.is_empty() { + // nothing to do + return; + } + self.swap_layouts.set_is_tiled_damaged(); // TODO: verify we can do all the below first + + // + 1 for the root pane + let mut stack_geoms = self + .tiled_panes + .stack_panes(root_pane_id, panes_to_stack.len() + 1); + if stack_geoms.is_empty() { + log::error!("Failed to find room for stacked panes"); + return; + } + self.tiled_panes + .set_geom_for_pane_with_id(&root_pane_id, stack_geoms.remove(0)); + let mut focused_pane_id_in_stack = None; + for mut pane in panes_to_stack.drain(..) { + let pane_id = pane.pid(); + let stack_geom = stack_geoms.remove(0); + pane.set_geom(stack_geom); + self.tiled_panes.add_pane_with_existing_geom(pane_id, pane); + if self.tiled_panes.pane_id_is_focused(&pane_id) { + focused_pane_id_in_stack = Some(pane_id); + } + } + // if we had a focused pane in the stack, we expand it + if let Some(focused_pane_id_in_stack) = focused_pane_id_in_stack { + self.tiled_panes + .expand_pane_in_stack(focused_pane_id_in_stack); + } else if self.tiled_panes.pane_id_is_focused(&root_pane_id) { + self.tiled_panes.expand_pane_in_stack(root_pane_id); + } + } fn new_scrollback_editor_pane(&self, pid: u32) -> TerminalPane { let next_terminal_position = self.get_next_terminal_position(); let mut new_pane = TerminalPane::new( diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 38140b04..d4b9e916 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -3647,3 +3647,43 @@ pub fn screen_can_move_pane_to_a_new_tab_left() { } assert_snapshot!(format!("{}", snapshot_count)); } + +#[test] +pub fn send_cli_stack_panes_action() { + let size = Size { cols: 80, rows: 10 }; + let client_id = 10; // fake client id should not appear in the screen's state + let mut initial_layout = TiledPaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + initial_layout.children = vec![ + TiledPaneLayout::default(), + TiledPaneLayout::default(), + TiledPaneLayout::default(), + TiledPaneLayout::default(), + TiledPaneLayout::default(), + ]; + let mut mock_screen = MockScreen::new(size); + let session_metadata = mock_screen.clone_session_metadata(); + let screen_thread = mock_screen.run(Some(initial_layout), vec![]); + let received_server_instructions = Arc::new(Mutex::new(vec![])); + let server_receiver = mock_screen.server_receiver.take().unwrap(); + let server_thread = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + let stack_panes_action = CliAction::StackPanes { + pane_ids: vec!["1".to_owned(), "2".to_owned(), "3".to_owned()], + }; + send_cli_action_to_server(&session_metadata, stack_panes_action, client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); + mock_screen.teardown(vec![server_thread, screen_thread]); + let snapshots = take_snapshots_and_cursor_coordinates_from_render_events( + received_server_instructions.lock().unwrap().iter(), + size, + ); + let snapshot_count = snapshots.len(); + for (_cursor_coordinates, snapshot) in snapshots { + assert_snapshot!(format!("{}", snapshot)); + } + assert_snapshot!(format!("{}", snapshot_count)); +} diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action-2.snap new file mode 100644 index 00000000..c26d308f --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action-2.snap @@ -0,0 +1,16 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3687 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ─────┐┌ Pane #2 ─────────────────────────────────────┐┌ Pane #5 ─────┐ +01 (C): │ │┌ Pane #3 ─────────────────────────────────────┐│ │ +02 (C): │ │┌ Pane #4 ─────────────────────────────────────┐│ │ +03 (C): │ ││ ││ │ +04 (C): │ ││ ││ │ +05 (C): │ ││ ││ │ +06 (C): │ ││ ││ │ +07 (C): │ ││ ││ │ +08 (C): │ ││ ││ │ +09 (C): └──────────────┘└──────────────────────────────────────────────┘└──────────────┘ + diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action-3.snap new file mode 100644 index 00000000..6329f4de --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action-3.snap @@ -0,0 +1,6 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3689 +expression: "format!(\"{}\", snapshot_count)" +--- +2 diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action.snap new file mode 100644 index 00000000..8b57c658 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action.snap @@ -0,0 +1,16 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +assertion_line: 3687 +expression: "format!(\"{}\", snapshot)" +--- +00 (C): ┌ Pane #1 ─────┐┌ Pane #2 ─────┐┌ Pane #3 ─────┐┌ Pane #4 ─────┐┌ Pane #5 ─────┐ +01 (C): │ ││ ││ ││ ││ │ +02 (C): │ ││ ││ ││ ││ │ +03 (C): │ ││ ││ ││ ││ │ +04 (C): │ ││ ││ ││ ││ │ +05 (C): │ ││ ││ ││ ││ │ +06 (C): │ ││ ││ ││ ││ │ +07 (C): │ ││ ││ ││ ││ │ +08 (C): │ ││ ││ ││ ││ │ +09 (C): └──────────────┘└──────────────┘└──────────────┘└──────────────┘└──────────────┘ + diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 7b33b8f9..5a97a442 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -1142,6 +1142,13 @@ pub fn set_floating_pane_pinned(pane_id: PaneId, should_be_pinned: bool) { unsafe { host_run_plugin_command() }; } +pub fn stack_panes(pane_ids: Vec) { + let plugin_command = PluginCommand::StackPanes(pane_ids); + 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 a023a68f..ac5d2c96 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -5,7 +5,7 @@ pub struct PluginCommand { 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" + 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" )] pub payload: ::core::option::Option, } @@ -178,10 +178,18 @@ pub mod plugin_command { ChangeHostFolderPayload(super::ChangeHostFolderPayload), #[prost(message, tag = "90")] SetFloatingPanePinnedPayload(super::SetFloatingPanePinnedPayload), + #[prost(message, tag = "91")] + StackPanesPayload(super::StackPanesPayload), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct StackPanesPayload { + #[prost(message, repeated, tag = "1")] + pub pane_ids: ::prost::alloc::vec::Vec, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct SetFloatingPanePinnedPayload { #[prost(message, optional, tag = "1")] pub pane_id: ::core::option::Option, @@ -738,6 +746,7 @@ pub enum CommandName { ListClients = 113, ChangeHostFolder = 114, SetFloatingPanePinned = 115, + StackPanes = 116, } impl CommandName { /// String value of the enum field names used in the ProtoBuf definition. @@ -864,6 +873,7 @@ impl CommandName { CommandName::ListClients => "ListClients", CommandName::ChangeHostFolder => "ChangeHostFolder", CommandName::SetFloatingPanePinned => "SetFloatingPanePinned", + CommandName::StackPanes => "StackPanes", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -987,6 +997,7 @@ impl CommandName { "ListClients" => Some(Self::ListClients), "ChangeHostFolder" => Some(Self::ChangeHostFolder), "SetFloatingPanePinned" => Some(Self::SetFloatingPanePinned), + "StackPanes" => Some(Self::StackPanes), _ => None, } } diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 1577be7c..38d51f6d 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -760,4 +760,15 @@ tail -f /tmp/my-live-logfile | zellij action pipe --name logs --plugin https://e }, ListClients, TogglePanePinned, + /// Stack pane ids + /// Ids are a space separated list of pane ids. + /// They should either be in the form of `terminal_` (eg. terminal_1), `plugin_` (eg. + /// plugin_1) or bare integers in which case they'll be considered terminals (eg. 1 is + /// the equivalent of terminal_1) + /// + /// Example: zellij action stack-panes -- terminal_1 plugin_2 3 + StackPanes { + #[clap(last(true), required(true))] + pane_ids: Vec, + }, } diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 1f3adf30..de14de02 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -1902,4 +1902,5 @@ pub enum PluginCommand { ListClients, ChangeHostFolder(PathBuf), SetFloatingPanePinned(PaneId, bool), // bool -> should be pinned + StackPanes(Vec), } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index f27ee06c..bd82b61c 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -375,6 +375,7 @@ pub enum ScreenContext { ListClientsToPlugin, TogglePanePinned, SetFloatingPanePinned, + StackPanes, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index f6ecf096..95ec6eb2 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -6,7 +6,7 @@ use super::layout::{ SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }; use crate::cli::CliAction; -use crate::data::{Direction, KeyWithModifier, Resize}; +use crate::data::{Direction, KeyWithModifier, PaneId, Resize}; use crate::data::{FloatingPaneCoordinates, InputMode}; use crate::home::{find_default_config_dir, get_layout_dir}; use crate::input::config::{Config, ConfigError, KdlError}; @@ -301,6 +301,7 @@ pub enum Action { }, ListClients, TogglePanePinned, + StackPanes(Vec), } impl Action { @@ -742,6 +743,53 @@ impl Action { }, CliAction::ListClients => Ok(vec![Action::ListClients]), CliAction::TogglePanePinned => Ok(vec![Action::TogglePanePinned]), + CliAction::StackPanes { pane_ids } => { + let mut malformed_ids = vec![]; + let pane_ids = pane_ids + .iter() + .filter_map(|stringified_pane_id| { + if let Some(terminal_pane_id) = + stringified_pane_id.strip_prefix("terminal_") + { + u32::from_str_radix(terminal_pane_id, 10) + .ok() + .or_else(|| { + malformed_ids.push(stringified_pane_id.to_owned()); + None + }) + .map(|id| PaneId::Terminal(id)) + } else if let Some(plugin_pane_id) = + stringified_pane_id.strip_prefix("plugin_") + { + u32::from_str_radix(plugin_pane_id, 10) + .ok() + .or_else(|| { + malformed_ids.push(stringified_pane_id.to_owned()); + None + }) + .map(|id| PaneId::Plugin(id)) + } else { + u32::from_str_radix(stringified_pane_id, 10) + .ok() + .or_else(|| { + malformed_ids.push(stringified_pane_id.to_owned()); + None + }) + .map(|id| PaneId::Terminal(id)) + } + }) + .collect(); + if !malformed_ids.is_empty() { + Err( + format!( + "Malformed pane ids: {}, expecting a space separated list of either a bare integer (eg. 1), a terminal pane id (eg. terminal_1) or a plugin pane id (eg. plugin_1)", + malformed_ids.join(", ") + ) + ) + } else { + Ok(vec![Action::StackPanes(pane_ids)]) + } + }, } } pub fn launches_plugin(&self, plugin_url: &str) -> bool { diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index 96385433..e3c05130 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -1301,6 +1301,7 @@ impl TryFrom for ProtobufAction { | Action::DumpLayout | Action::CliPipe { .. } | Action::ListClients + | Action::StackPanes(..) | Action::SkipConfirm(..) => Err("Unsupported action"), } } diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index 14e8a286..7572476d 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -129,6 +129,7 @@ enum CommandName { ListClients = 113; ChangeHostFolder = 114; SetFloatingPanePinned = 115; + StackPanes = 116; } message PluginCommand { @@ -214,9 +215,14 @@ message PluginCommand { RebindKeysPayload rebind_keys_payload = 88; ChangeHostFolderPayload change_host_folder_payload = 89; SetFloatingPanePinnedPayload set_floating_pane_pinned_payload = 90; + StackPanesPayload stack_panes_payload = 91; } } +message StackPanesPayload { + repeated PaneId pane_ids = 1; +} + message SetFloatingPanePinnedPayload { PaneId pane_id = 1; bool should_be_pinned = 2; diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index 9d92f260..2621502a 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -19,9 +19,10 @@ pub use super::generated_api::api::{ RerunCommandPanePayload, ResizePaneIdWithDirectionPayload, ResizePayload, RunCommandPayload, ScrollDownInPaneIdPayload, ScrollToBottomInPaneIdPayload, ScrollToTopInPaneIdPayload, ScrollUpInPaneIdPayload, SetFloatingPanePinnedPayload, - SetTimeoutPayload, ShowPaneWithIdPayload, SubscribePayload, SwitchSessionPayload, - SwitchTabToPayload, TogglePaneEmbedOrEjectForPaneIdPayload, TogglePaneIdFullscreenPayload, - UnsubscribePayload, WebRequestPayload, WriteCharsToPaneIdPayload, WriteToPaneIdPayload, + SetTimeoutPayload, ShowPaneWithIdPayload, StackPanesPayload, SubscribePayload, + SwitchSessionPayload, SwitchTabToPayload, TogglePaneEmbedOrEjectForPaneIdPayload, + TogglePaneIdFullscreenPayload, UnsubscribePayload, WebRequestPayload, + WriteCharsToPaneIdPayload, WriteToPaneIdPayload, }, plugin_permission::PermissionType as ProtobufPermissionType, resize::ResizeAction as ProtobufResizeAction, @@ -1328,6 +1329,18 @@ impl TryFrom for PluginCommand { }, _ => Err("Mismatched payload for SetFloatingPanePinned"), }, + Some(CommandName::StackPanes) => match protobuf_plugin_command.payload { + Some(Payload::StackPanesPayload(stack_panes_payload)) => { + Ok(PluginCommand::StackPanes( + stack_panes_payload + .pane_ids + .into_iter() + .filter_map(|p_id| p_id.try_into().ok()) + .collect(), + )) + }, + _ => Err("Mismatched payload for SetFloatingPanePinned"), + }, None => Err("Unrecognized plugin command"), } } @@ -2172,6 +2185,15 @@ impl TryFrom for ProtobufPluginCommand { )), }) }, + PluginCommand::StackPanes(pane_ids) => Ok(ProtobufPluginCommand { + name: CommandName::StackPanes as i32, + payload: Some(Payload::StackPanesPayload(StackPanesPayload { + pane_ids: pane_ids + .into_iter() + .filter_map(|p_id| p_id.try_into().ok()) + .collect(), + })), + }), } } }