aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2024-12-31 12:37:23 +0100
committerGitHub <noreply@github.com>2024-12-31 12:37:23 +0100
commit42adc8cd048c03d52a9f33503d75e24364adbc68 (patch)
tree943ee11e188e6512b336b603bdd5ea8eae38998b
parentdocs(changelog): cursor overflow fix (diff)
downloadsl0ppij-42adc8cd048c03d52a9f33503d75e24364adbc68.tar.gz
sl0ppij-42adc8cd048c03d52a9f33503d75e24364adbc68.tar.xz
sl0ppij-42adc8cd048c03d52a9f33503d75e24364adbc68.zip
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
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs21
-rw-r--r--zellij-server/src/panes/tiled_panes/stacked_panes.rs30
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs10
-rw-r--r--zellij-server/src/route.rs7
-rw-r--r--zellij-server/src/screen.rs68
-rw-r--r--zellij-server/src/tab/mod.rs51
-rw-r--r--zellij-server/src/unit/screen_tests.rs40
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action-2.snap16
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action-3.snap6
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_stack_panes_action.snap16
-rw-r--r--zellij-tile/src/shim.rs7
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs13
-rw-r--r--zellij-utils/src/cli.rs11
-rw-r--r--zellij-utils/src/data.rs1
-rw-r--r--zellij-utils/src/errors.rs1
-rw-r--r--zellij-utils/src/input/actions.rs50
-rw-r--r--zellij-utils/src/plugin_api/action.rs1
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto6
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs28
19 files changed, 373 insertions, 10 deletions
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<PaneGeom> {
+ 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<PaneGeom> {
+ 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<Vec<Vec<(PaneId, PaneGeom)>>> {
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<PaneId>) {
+ 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<PaneId>),
}
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<InitialTitle>,
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<PaneId>) {
+ 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<ClientId>) {
+ pub fn suppress_pane(&mut self, pane_id: PaneId, _client_id: Option<ClientId>) {
// 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<Box<dyn Pane>>) {
+ 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<PaneId>) {
+ 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<plugin_command::Payload>,
}
@@ -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<PaneId>,
+}
+#[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<PaneId>,
@@ -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_<int>` (eg. terminal_1), `plugin_<int>` (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<String>,
+ },
}
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<PaneId>),
}
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<PaneId>),
}
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<Action> 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<ProtobufPluginCommand> 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<PluginCommand> 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(),
+ })),
+ }),
}
}
}