diff options
| author | DawidPietrykowski <53954695+DawidPietrykowski@users.noreply.github.com> | 2026-06-25 18:12:51 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-25 18:12:51 +0200 |
| commit | ebd731b0ff12737a6e2160a255280534ef3991bd (patch) | |
| tree | 4313963a269205391aa519fd090ea1de2b7cc4af /zellij-server | |
| parent | tests: add more integration tests (#5308) (diff) | |
| download | sl0ppij-ebd731b0ff12737a6e2160a255280534ef3991bd.tar.gz sl0ppij-ebd731b0ff12737a6e2160a255280534ef3991bd.tar.xz sl0ppij-ebd731b0ff12737a6e2160a255280534ef3991bd.zip | |
feat(panes): `FocusLastPane` action (#4241)
* feat(panes): focus last pane (#4142)
* add tests
* add client id parameter to `add_floating_pane`
* add action for floating panes
* add tests for floating panes
* add keybinds to configs
* rebase fixes
* simplify focus logic when adding stacked pane
* formatting fix
* formatting fix 2
* fix: make sure expanding pane always focuses it for all clients in stack
* add pr
* fix rebase
---------
Co-authored-by: Aram Drevekenin <aram@poor.dev>
Diffstat (limited to 'zellij-server')
20 files changed, 655 insertions, 17 deletions
diff --git a/zellij-server/src/panes/active_panes.rs b/zellij-server/src/panes/active_panes.rs index 01ce65aa..159412b5 100644 --- a/zellij-server/src/panes/active_panes.rs +++ b/zellij-server/src/panes/active_panes.rs @@ -7,6 +7,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; pub struct ActivePanes { active_panes: HashMap<ClientId, PaneId>, shadow_clients: HashSet<ClientId>, + last_panes: HashMap<ClientId, PaneId>, os_api: Box<dyn ServerOsApi>, } @@ -22,6 +23,7 @@ impl ActivePanes { ActivePanes { active_panes: HashMap::new(), shadow_clients: HashSet::new(), + last_panes: HashMap::new(), os_api, } } @@ -38,6 +40,9 @@ impl ActivePanes { pub fn get(&self, client_id: &ClientId) -> Option<&PaneId> { self.active_panes.get(client_id) } + pub fn get_last(&self, client_id: &ClientId) -> Option<&PaneId> { + self.last_panes.get(client_id) + } pub fn insert( &mut self, client_id: ClientId, @@ -57,6 +62,9 @@ impl ActivePanes { self.shadow_clients.remove(client_id); self.active_panes.remove(client_id) } + pub fn set_last_pane(&mut self, client_id: ClientId, pane_id: PaneId) { + self.last_panes.insert(client_id, pane_id); + } pub fn clear(&mut self, panes: &mut BTreeMap<PaneId, Box<dyn Pane>>) { for pane_id in self.active_panes.values() { self.unfocus_pane(*pane_id, panes); diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 438e77fe..4093e488 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -739,6 +739,13 @@ impl FloatingPanes { } } + pub fn focus_last_pane(&mut self, client_id: ClientId) { + if let Some(pane_id) = self.active_panes.get_last(&client_id).copied() { + self.focus_pane(pane_id, client_id); + self.set_force_render(); + } + } + pub fn move_active_pane_down(&mut self, client_id: ClientId) { if let Some(active_pane_id) = self.active_panes.get(&client_id) { self.move_pane_down(*active_pane_id); @@ -957,6 +964,11 @@ impl FloatingPanes { self.set_force_render(); } pub fn focus_pane(&mut self, pane_id: PaneId, client_id: ClientId) { + if let Some(focused_pane) = self.active_panes.get(&client_id) { + if pane_id != *focused_pane { + self.active_panes.set_last_pane(client_id, *focused_pane); + } + } let pane_is_selectable = self .panes .get(&pane_id) diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index ff68f750..04ca0a8b 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -764,9 +764,7 @@ impl TiledPanes { let _ = StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide) .expand_pane(&pane_id); } - self.active_panes - .insert(client_id, pane_id, &mut self.panes); - self.set_pane_active_at(pane_id); + self.focus_pane(pane_id, client_id); } self.set_force_render(); self.reapply_pane_frames(); @@ -866,8 +864,14 @@ impl TiledPanes { let connected_clients: Vec<ClientId> = self.connected_clients.borrow().iter().copied().collect(); for client_id in connected_clients { - if let Some(focused_pane_id_for_client) = self.active_panes.get(&client_id) { - if all_panes_in_stack.contains(focused_pane_id_for_client) { + if let Some(focused_pane_id_for_client) = + self.active_panes.get(&client_id).copied() + { + if all_panes_in_stack.contains(&focused_pane_id_for_client) { + if focused_pane_id_for_client != pane_id { + self.active_panes + .set_last_pane(client_id, focused_pane_id_for_client); + } self.active_panes .insert(client_id, pane_id, &mut self.panes); self.set_pane_active_at(pane_id); @@ -885,6 +889,11 @@ impl TiledPanes { } } pub fn focus_pane(&mut self, pane_id: PaneId, client_id: ClientId) { + if let Some(focused_pane) = self.active_panes.get(&client_id).copied() { + if pane_id != focused_pane { + self.active_panes.set_last_pane(client_id, focused_pane); + } + } let pane_is_selectable = self .panes .get(&pane_id) @@ -988,6 +997,9 @@ impl TiledPanes { pub fn get_active_pane_id(&self, client_id: ClientId) -> Option<PaneId> { self.active_panes.get(&client_id).copied() } + pub fn get_last_pane_id(&self, client_id: ClientId) -> Option<PaneId> { + self.active_panes.get_last(&client_id).copied() + } pub fn panes_contain(&self, pane_id: &PaneId) -> bool { self.panes.contains_key(pane_id) } @@ -1870,6 +1882,36 @@ impl TiledPanes { self.set_pane_active_at(next_active_pane_id); self.reset_boundaries(); } + pub fn focus_last_pane(&mut self, client_id: ClientId) { + let Some(last_pane_id) = self.get_last_pane_id(client_id) else { + return; + }; + + let previously_active_pane_id = self.active_panes.get(&client_id).unwrap(); + let previously_active_pane = self.panes.get_mut(previously_active_pane_id).unwrap(); + + previously_active_pane.set_should_render(true); + // we render the full viewport to remove any ui elements that might have been + // there before (eg. another user's cursor) + previously_active_pane.render_full_viewport(); + + let next_active_pane = self.panes.get_mut(&last_pane_id).unwrap(); + let stacked = next_active_pane.current_geom().stacked; + next_active_pane.set_should_render(true); + // we render the full viewport to remove any ui elements that might have been + // there before (eg. another user's cursor) + next_active_pane.render_full_viewport(); + + self.focus_pane(last_pane_id, client_id); + self.set_pane_active_at(last_pane_id); + if let Some(stack_id) = stacked { + // we do this because a stack pane focus change also changes its + // geometry and we need to let the pty know about this (like in a + // normal size change) + self.focus_pane_for_all_clients_in_stack(last_pane_id, stack_id); + self.reapply_pane_frames(); + } + } fn set_pane_active_at(&mut self, pane_id: PaneId) { if let Some(pane) = self.get_pane_mut(pane_id) { pane.set_active_at(Instant::now()); @@ -2648,6 +2690,12 @@ impl TiledPanes { self.toggle_active_pane_fullscreen(client_id); } + pub fn switch_last_pane_fullscreen(&mut self, client_id: ClientId) { + self.unset_fullscreen(); + self.focus_last_pane(client_id); + self.toggle_active_pane_fullscreen(client_id); + } + pub fn panes_to_hide_count(&self) -> usize { self.panes_to_hide.len() } diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 35d80ecf..431d550e 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -1901,6 +1901,86 @@ pub fn focus_previous_pane_plugin_command() { #[test] #[ignore] +pub fn focus_last_pane_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, screen_receiver, teardown) = + create_plugin_thread(Some(plugin_host_folder), None); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + ..Default::default() + }); + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let screen_thread = grant_permissions_and_log_actions_in_thread!( + received_screen_instructions, + ScreenInstruction::FocusLastPane, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + false, // close_replaced_pane + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + None, + false, + None, + None, + None, + )); + std::thread::sleep(std::time::Duration::from_millis(500)); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key( + KeyWithModifier::new(BareKey::Char('l')) + .with_alt_modifier() + .with_ctrl_modifier(), + ), // this triggers the enent in the fixture plugin + )])); + screen_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_screen_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let ScreenInstruction::FocusLastPane(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] pub fn move_focus_plugin_command() { let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its // destructor removes the directory diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_last_pane_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_last_pane_plugin_command.snap new file mode 100644 index 00000000..a4b3bb9e --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_last_pane_plugin_command.snap @@ -0,0 +1,21 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 1990 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + FocusLastPane( + 1, + Some( + NotificationEnd { + channel: None, + exit_status: None, + unblock_condition: None, + affected_pane_id: None, + affected_tab_id: None, + error_message: None, + stdout_message: None, + }, + ), + ), +) diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index ce53592e..c9c3515e 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -312,6 +312,7 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) { }, PluginCommand::FocusNextPane => focus_next_pane(env), PluginCommand::FocusPreviousPane => focus_previous_pane(env), + PluginCommand::FocusLastPane => focus_last_pane(env), PluginCommand::MoveFocus(direction) => move_focus(env, direction), PluginCommand::MoveFocusOrTab(direction) => move_focus_or_tab(env, direction), PluginCommand::Detach => detach(env), @@ -2920,6 +2921,12 @@ fn focus_previous_pane(env: &PluginEnv) { apply_action!(action, error_msg, env); } +fn focus_last_pane(env: &PluginEnv) { + let action = Action::FocusLastPane; + let error_msg = || format!("Failed to focus last pane"); + apply_action!(action, error_msg, env); +} + fn move_focus(env: &PluginEnv, direction: Direction) { let error_msg = || format!("failed to move focus in plugin {}", env.name()); let action = Action::MoveFocus { direction }; diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 087a048e..9b9de0ec 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -374,6 +374,14 @@ pub(crate) fn route_action( )) .with_context(err_context)?; }, + Action::FocusLastPane => { + senders + .send_to_screen(ScreenInstruction::FocusLastPane( + client_id, + Some(NotificationEnd::new(completion_tx)), + )) + .with_context(err_context)?; + }, Action::MoveFocus { direction } => { let notification_end = Some(NotificationEnd::new(completion_tx)); diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index e287cae9..e6bbbf82 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -365,6 +365,7 @@ pub enum ScreenInstruction { SwitchFocus(ClientId, Option<NotificationEnd>), FocusNextPane(ClientId, Option<NotificationEnd>), FocusPreviousPane(ClientId, Option<NotificationEnd>), + FocusLastPane(ClientId, Option<NotificationEnd>), MoveFocusLeft(ClientId, Option<NotificationEnd>), MoveFocusLeftOrPreviousTab(ClientId, Option<NotificationEnd>), MoveFocusDown(ClientId, Option<NotificationEnd>), @@ -942,6 +943,7 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::SwitchFocus(..) => ScreenContext::SwitchFocus, ScreenInstruction::FocusNextPane(..) => ScreenContext::FocusNextPane, ScreenInstruction::FocusPreviousPane(..) => ScreenContext::FocusPreviousPane, + ScreenInstruction::FocusLastPane(..) => ScreenContext::FocusLastPane, ScreenInstruction::MoveFocusLeft(..) => ScreenContext::MoveFocusLeft, ScreenInstruction::MoveFocusLeftOrPreviousTab(..) => { ScreenContext::MoveFocusLeftOrPreviousTab @@ -4465,6 +4467,7 @@ impl Screen { pane_id, None, true, + Some(client_id), )?; // TODO: also should_be_in_place } else { @@ -4744,7 +4747,13 @@ impl Screen { if pane_to_break_is_floating { new_active_tab.show_floating_panes(); - new_active_tab.add_floating_pane(active_pane, active_pane_id, None, true)?; + new_active_tab.add_floating_pane( + active_pane, + active_pane_id, + None, + true, + Some(client_id), + )?; } else { new_active_tab.hide_floating_panes(); new_active_tab.add_tiled_pane( @@ -4831,6 +4840,7 @@ impl Screen { pane_id, Some(floating_pane_coordinates), false, + Some(client_id), )?; } else { // here we pass None instead of the ClientId, because we do not want this pane to be @@ -6661,6 +6671,19 @@ pub(crate) fn screen_thread_main( screen.log_and_report_session_state()?; } }, + ScreenInstruction::FocusLastPane( + client_id, + _completion_tx, // the action ends here, dropping this will release anything + // waiting for it + ) => { + active_tab_and_connected_client_id!( + screen, + client_id, + |tab: &mut Tab, client_id: ClientId| tab.focus_last_pane(client_id) + ); + screen.render(None)?; + screen.log_and_report_session_state()?; + }, ScreenInstruction::MoveFocusLeft(client_id, mut _completion_tx) => { if screen.get_first_client_id().is_none() { log::error!("No connected clients to move focus for"); diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index a1184635..02e7bece 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -1376,7 +1376,13 @@ impl Tab { } if let Some(embedded_pane_to_float) = self.extract_pane(focused_pane_id, true) { self.show_floating_panes(); - self.add_floating_pane(embedded_pane_to_float, focused_pane_id, None, true)?; + self.add_floating_pane( + embedded_pane_to_float, + focused_pane_id, + None, + true, + Some(client_id), + )?; } } Ok(()) @@ -1412,7 +1418,7 @@ impl Tab { return Ok(()); } if let Some(embedded_pane_to_float) = self.extract_pane(pane_id, true) { - self.add_floating_pane(embedded_pane_to_float, pane_id, None, true)?; + self.add_floating_pane(embedded_pane_to_float, pane_id, None, true, client_id)?; } } Ok(()) @@ -1677,13 +1683,13 @@ impl Tab { Ok(()) } else if should_focus_pane { if self.floating_panes.panes_are_visible() { - self.add_floating_pane(new_pane, pid, None, true) + self.add_floating_pane(new_pane, pid, None, true, client_id) } else { self.add_tiled_pane(new_pane, pid, false, client_id) } } else { if self.floating_panes.panes_are_visible() { - self.add_floating_pane(new_pane, pid, None, false) + self.add_floating_pane(new_pane, pid, None, false, client_id) } else { self.add_tiled_pane(new_pane, pid, false, client_id) } @@ -1887,7 +1893,13 @@ impl Tab { .insert(pid, (is_scrollback_editor, new_pane)); Ok(()) } else { - self.add_floating_pane(new_pane, pid, floating_pane_coordinates, should_focus_pane) + self.add_floating_pane( + new_pane, + pid, + floating_pane_coordinates, + should_focus_pane, + None, + ) } } pub fn new_in_place_pane( @@ -3287,6 +3299,12 @@ impl Tab { } self.tiled_panes.switch_prev_pane_fullscreen(client_id); } + pub fn switch_last_pane_fullscreen(&mut self, client_id: ClientId) { + if !self.is_fullscreen_active() { + return; + } + self.tiled_panes.switch_last_pane_fullscreen(client_id); + } pub fn set_force_render(&mut self) { self.tiled_panes.set_force_render(); self.floating_panes.set_force_render(); @@ -3662,6 +3680,18 @@ impl Tab { } self.tiled_panes.focus_previous_pane(client_id); } + pub fn focus_last_pane(&mut self, client_id: ClientId) { + if !self.has_selectable_panes() { + return; + } + if self.floating_panes.panes_are_visible() { + self.floating_panes.focus_last_pane(client_id); + } else if self.tiled_panes.fullscreen_is_active() { + self.switch_last_pane_fullscreen(client_id); + return; + } + self.tiled_panes.focus_last_pane(client_id); + } pub fn focus_pane_on_edge(&mut self, direction: Direction, client_id: ClientId) { if self.floating_panes.panes_are_visible() { self.floating_panes.focus_pane_on_edge(direction, client_id); @@ -5357,7 +5387,7 @@ impl Tab { pane.set_selectable(true); if should_float { self.show_floating_panes(); - self.add_floating_pane(pane, pane_id, None, true) + self.add_floating_pane(pane, pane_id, None, true, Some(client_id)) } else if should_be_in_place { let replaced_pane = if self.are_floating_panes_visible() { self.floating_panes @@ -5389,7 +5419,7 @@ impl Tab { match self.suppressed_panes.remove(&pane_id) { Some(pane) => { self.show_floating_panes(); - self.add_floating_pane(pane.1, pane_id, None, true) + self.add_floating_pane(pane.1, pane_id, None, true, None) .non_fatal(); self.floating_panes.focus_pane_for_all_clients(pane_id); }, @@ -5417,7 +5447,7 @@ impl Tab { { Some(pane) => { if should_float_if_hidden { - self.add_floating_pane(pane, pane_id, None, true) + self.add_floating_pane(pane, pane_id, None, true, None) .non_fatal(); } else { self.add_tiled_pane(pane, pane_id, false, None).non_fatal(); @@ -5438,7 +5468,7 @@ impl Tab { { Some(pane) => { if should_float_if_hidden { - self.add_floating_pane(pane, pane_id, None, true) + self.add_floating_pane(pane, pane_id, None, true, None) .non_fatal(); } else { self.add_tiled_pane(pane, pane_id, false, None).non_fatal(); @@ -5525,6 +5555,7 @@ impl Tab { pane_id: PaneId, floating_pane_coordinates: Option<FloatingPaneCoordinates>, should_focus_new_pane: bool, + client_id: Option<ClientId>, ) -> Result<()> { let err_context = || format!("failed to add floating pane"); if let Some(mut new_pane_geom) = self.floating_panes.find_room_for_new_pane() { @@ -5552,7 +5583,11 @@ impl Tab { .with_context(err_context)?; self.floating_panes.add_pane(pane_id, pane); if should_focus_new_pane { - self.floating_panes.focus_pane_for_all_clients(pane_id); + if let Some(client_id) = client_id { + self.floating_panes.focus_pane(pane_id, client_id); + } else { + self.floating_panes.focus_pane_for_all_clients(pane_id); + } } } if self.auto_layout && !self.swap_layouts.is_floating_damaged() { @@ -5916,7 +5951,7 @@ impl Tab { || self.suppressed_panes.contains_key(pane_id) { if let Some(pane) = self.extract_pane(*pane_id, true) { - self.add_floating_pane(pane, *pane_id, None, false)?; + self.add_floating_pane(pane, *pane_id, None, false, None)?; } } } diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__focus_last_stacked_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__focus_last_stacked_pane.snap new file mode 100644 index 00000000..190ec390 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__focus_last_stacked_pane.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +01 (C): │ │┌ Pane #3 ─────────────────────────────────────────────────┐ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ │└──────────────────────────────────────────────────────────┘ +19 (C): └───────────────────────────────────────────────────────────┘└ Pane #4 ─────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_to_last_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_to_last_pane.snap new file mode 100644 index 00000000..1f1604ef --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_to_last_pane.snap @@ -0,0 +1,24 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ ┌ Pane #5 ───── SCROLL: 0/1 | PIN [ ] ┐ │ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +04 (C): │ │E┌ Pane #6 ───── SCROLL: 0/1 | PIN [ ] ┐ │ +05 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│──────────────────────────────────── PIN [ ] ┐ │ +06 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ │ +07 (C): │ └─│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│─────────────────────── SCROLL: 0/1 | PIN [ ] ┐ │ +08 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +09 (C): │ └──────────────────────────────────────┘───────────────────────── SCROLL: 0/1 | PIN [ ] ┐ │ +10 (C): │ │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +11 (C): │ │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +12 (C): │ │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +13 (C): │ │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ └─│E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │E│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ └─│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +17 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +18 (C): │ └──────────────────────────────────────────────────────────┘ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 80b6324e..c9a7508c 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -2431,6 +2431,108 @@ fn move_floating_pane_focus_with_mouse() { } #[test] +fn move_floating_pane_focus_to_last_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size, ModeInfo::default()); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(Some(client_id), None, None) + .unwrap(); + tab.new_pane( + new_pane_id_1, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + tab.new_pane( + new_pane_id_2, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + tab.new_pane( + new_pane_id_3, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + tab.new_pane( + new_pane_id_4, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + tab.new_pane( + new_pane_id_5, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ) + .unwrap(); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) + .unwrap(); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) + .unwrap(); + tab.move_focus_up(client_id).unwrap(); + tab.focus_last_pane(client_id); + tab.render(&mut output, None).unwrap(); + let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_eq!( + cursor_coordinates, + Some((5, 5)), + "cursor coordinates moved back to the last pane below" + ); + + assert_snapshot!(snapshot); +} + +#[test] fn move_pane_focus_with_mouse_to_non_floating_pane() { let size = Size { cols: 121, @@ -5803,6 +5905,89 @@ fn move_focus_down_into_stacked_panes() { } #[test] +fn focus_last_stacked_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut output = Output::default(); + let swap_layouts = r#" + layout { + swap_tiled_layout { + tab { + pane split_direction="vertical" { + pane focus=true + pane stacked=true { children; } + } + } + } + } + "#; + let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); + let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); + let swap_floating_layouts = layout.swap_floating_layouts.clone(); + let stacked_resize = true; + let mut tab = create_new_tab_with_swap_layouts( + size, + ModeInfo::default(), + (swap_tiled_layouts, swap_floating_layouts), + None, + true, + stacked_resize, + ); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + + tab.new_pane( + new_pane_id_1, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + tab.new_pane( + new_pane_id_2, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + tab.new_pane( + new_pane_id_3, + None, + None, + false, + true, + NewPanePlacement::default(), + Some(client_id), + None, + ) + .unwrap(); + tab.move_focus_right(client_id); + tab.move_focus_up(client_id); + tab.move_focus_up(client_id); + tab.focus_last_pane(client_id); + tab.render(&mut output, None).unwrap(); + let snapshot = take_snapshot( + output.serialize().unwrap().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] fn close_main_stacked_pane() { let size = Size { cols: 121, diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index 1ce0ed72..4651c0a6 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -1568,6 +1568,90 @@ fn switch_to_prev_pane_fullscreen() { } #[test] +fn switch_to_last_pane_fullscreen() { + let size = Size { + cols: 121, + rows: 20, + }; + let stacked_resize = true; + let mut active_tab = create_new_tab(size, stacked_resize); + + //testing four consecutive switches in fullscreen mode + + active_tab + .new_pane( + PaneId::Terminal(1), + None, + None, + false, + true, + NewPanePlacement::default(), + Some(1), + None, + ) + .unwrap(); + active_tab + .new_pane( + PaneId::Terminal(2), + None, + None, + false, + true, + NewPanePlacement::default(), + Some(1), + None, + ) + .unwrap(); + active_tab + .new_pane( + PaneId::Terminal(3), + None, + None, + false, + true, + NewPanePlacement::default(), + Some(1), + None, + ) + .unwrap(); + active_tab + .new_pane( + PaneId::Terminal(4), + None, + None, + false, + true, + NewPanePlacement::default(), + Some(1), + None, + ) + .unwrap(); + active_tab.toggle_active_pane_fullscreen(1); + + // order is now 1 2 3 4, current active is Terminal 4 + + active_tab.switch_last_pane_fullscreen(1); + + // the position should now be in Terminal 3 + + assert_eq!( + active_tab.get_active_pane_id(1).unwrap(), + PaneId::Terminal(3), + "Active pane did not switch to the last pane in fullscreen mode" + ); + + active_tab.switch_last_pane_fullscreen(1); + + // the position should now be back in Terminal 4 + + assert_eq!( + active_tab.get_active_pane_id(1).unwrap(), + PaneId::Terminal(4), + "Active pane did not switch to the last pane in fullscreen mode" + ); +} + +#[test] pub fn close_pane_with_another_pane_above_it() { // ┌───────────┐ ┌───────────┐ // │xxxxxxxxxxx│ │xxxxxxxxxxx│ diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 1d33e8fd..245cac8c 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -2473,6 +2473,55 @@ pub fn send_cli_focus_previous_pane_action() { } #[test] +pub fn send_cli_focus_last_pane_action() { + let size = Size { cols: 80, rows: 20 }; + 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(), + ]; + 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_instruction = log_actions_in_thread!( + received_server_instructions, + ServerInstruction::KillSession, + server_receiver + ); + let move_focus_action = CliAction::MoveFocus { + direction: Direction::Right, + }; + let focus_last_pane_action = CliAction::FocusLastPane; + // move focus 1 -> 2 -> 3 + send_cli_action_to_server(&session_metadata, move_focus_action.clone(), client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); // give time for the async render + send_cli_action_to_server(&session_metadata, move_focus_action.clone(), client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); // give time for the async render + // move focus 3 -> 2 + send_cli_action_to_server(&session_metadata, focus_last_pane_action.clone(), client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); // give time for the async render + // move focus 2 -> 3 + send_cli_action_to_server(&session_metadata, focus_last_pane_action.clone(), client_id); + std::thread::sleep(std::time::Duration::from_millis(100)); // give time for the async render + mock_screen.teardown(vec![server_instruction, 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 { + // here we assert he cursor_coordinates to let us know if we switched the pane focus + assert_snapshot!(format!("{:?}", cursor_coordinates)); + } + assert_snapshot!(format!("{}", snapshot_count)); +} + +#[test] pub fn send_cli_move_focus_pane_action() { let size = Size { cols: 80, rows: 20 }; let client_id = 10; // fake client id should not appear in the screen's state diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-2.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-2.snap new file mode 100644 index 00000000..4078f027 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-2.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +expression: "format!(\"{:?}\", cursor_coordinates)" +--- +Some((28, 1)) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-3.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-3.snap new file mode 100644 index 00000000..7c6ee1f3 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-3.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +expression: "format!(\"{:?}\", cursor_coordinates)" +--- +Some((55, 1)) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-4.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-4.snap new file mode 100644 index 00000000..4078f027 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-4.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +expression: "format!(\"{:?}\", cursor_coordinates)" +--- +Some((28, 1)) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-5.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-5.snap new file mode 100644 index 00000000..7c6ee1f3 --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-5.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +expression: "format!(\"{:?}\", cursor_coordinates)" +--- +Some((55, 1)) diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-6.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-6.snap new file mode 100644 index 00000000..84b05c9c --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-6.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +expression: "format!(\"{}\", snapshot_count)" +--- +5 diff --git a/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action.snap b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action.snap new file mode 100644 index 00000000..e627107f --- /dev/null +++ b/zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action.snap @@ -0,0 +1,5 @@ +--- +source: zellij-server/src/./unit/screen_tests.rs +expression: "format!(\"{:?}\", cursor_coordinates)" +--- +Some((1, 1)) |
