From 5bd959e51247d1b5d1ffa4b9f8c76e70f585b8ab Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Fri, 20 Jun 2025 01:20:11 +0200 Subject: [PATCH 1/6] feat(panes): focus last pane (#4142) --- docs/MANPAGE.md | 1 + .../src/old_config_converter/old_config.rs | 2 + zellij-server/src/panes/active_panes.rs | 12 +++++ zellij-server/src/panes/tiled_panes/mod.rs | 47 +++++++++++++++++++ zellij-server/src/plugins/zellij_exports.rs | 7 +++ zellij-server/src/route.rs | 5 ++ zellij-server/src/screen.rs | 12 +++++ zellij-server/src/tab/mod.rs | 16 +++++++ zellij-tile/src/shim.rs | 8 ++++ zellij-utils/assets/prost/api.action.rs | 3 ++ .../assets/prost/api.plugin_command.rs | 3 ++ zellij-utils/src/cli.rs | 2 + zellij-utils/src/data.rs | 1 + zellij-utils/src/errors.rs | 1 + zellij-utils/src/input/actions.rs | 3 ++ zellij-utils/src/kdl/mod.rs | 6 +++ ...__keybinds_to_string_with_all_actions.snap | 3 +- zellij-utils/src/plugin_api/action.proto | 1 + zellij-utils/src/plugin_api/action.rs | 8 ++++ .../src/plugin_api/plugin_command.proto | 1 + zellij-utils/src/plugin_api/plugin_command.rs | 10 ++++ 21 files changed, 150 insertions(+), 2 deletions(-) diff --git a/docs/MANPAGE.md b/docs/MANPAGE.md index 50c37d8061..7c1d6ad3e9 100644 --- a/docs/MANPAGE.md +++ b/docs/MANPAGE.md @@ -149,6 +149,7 @@ ACTIONS on screen edge. * __FocusPreviousPane__ - switches focus to the next pane to the left or above if on screen edge. +* __FocusLastPane__ - switches focus to the previously focused pane. * __SwitchFocus__ - left for legacy support. Switches focus to a pane with the next ID. * __MoveFocus: __ - moves focus in the specified direction (Left, diff --git a/zellij-client/src/old_config_converter/old_config.rs b/zellij-client/src/old_config_converter/old_config.rs index bfe345dfc4..7c9d49bdb2 100644 --- a/zellij-client/src/old_config_converter/old_config.rs +++ b/zellij-client/src/old_config_converter/old_config.rs @@ -997,6 +997,7 @@ enum OldAction { Resize(OldResizeDirection), FocusNextPane, FocusPreviousPane, + FocusLastPane, SwitchFocus, MoveFocus(OldDirection), MoveFocusOrTab(OldDirection), @@ -1068,6 +1069,7 @@ impl std::fmt::Display for OldAction { Self::Resize(resize_direction) => write!(f, "Resize \"{}\"", resize_direction), Self::FocusNextPane => write!(f, "FocusNextPane"), Self::FocusPreviousPane => write!(f, "FocusPreviousPane"), + Self::FocusLastPane => write!(f, "FocusLastPane"), Self::SwitchFocus => write!(f, "SwitchFocus"), Self::MoveFocus(direction) => write!(f, "MoveFocus \"{}\"", direction), Self::MoveFocusOrTab(direction) => write!(f, "MoveFocusOrTab \"{}\"", direction), diff --git a/zellij-server/src/panes/active_panes.rs b/zellij-server/src/panes/active_panes.rs index b0b09b06b1..b80d418a4b 100644 --- a/zellij-server/src/panes/active_panes.rs +++ b/zellij-server/src/panes/active_panes.rs @@ -6,6 +6,7 @@ use std::collections::{BTreeMap, HashMap}; #[derive(Clone)] pub struct ActivePanes { active_panes: HashMap, + last_panes: HashMap, os_api: Box, } @@ -20,12 +21,16 @@ impl ActivePanes { let os_api = os_api.clone(); ActivePanes { active_panes: HashMap::new(), + last_panes: HashMap::new(), os_api, } } 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, @@ -36,6 +41,13 @@ impl ActivePanes { self.active_panes.insert(client_id, pane_id); self.focus_pane(pane_id, panes); } + 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>) { for pane_id in self.active_panes.values() { self.unfocus_pane(*pane_id, panes); diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index e877026f5f..7e91add7b2 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -867,6 +867,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) @@ -970,6 +975,9 @@ impl TiledPanes { pub fn get_active_pane_id(&self, client_id: ClientId) -> Option { self.active_panes.get(&client_id).copied() } + pub fn get_last_pane_id(&self, client_id: ClientId) -> Option { + self.active_panes.get_last(&client_id).copied() + } pub fn panes_contain(&self, pane_id: &PaneId) -> bool { self.panes.contains_key(pane_id) } @@ -1747,6 +1755,39 @@ 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()); @@ -2513,6 +2554,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/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index ed43f0911d..14c6f97ab1 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -188,6 +188,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), @@ -1510,6 +1511,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 678c1558a4..2d26b14af8 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -132,6 +132,11 @@ pub(crate) fn route_action( .send_to_screen(ScreenInstruction::FocusPreviousPane(client_id)) .with_context(err_context)?; }, + Action::FocusLastPane => { + senders + .send_to_screen(ScreenInstruction::FocusLastPane(client_id)) + .with_context(err_context)?; + }, Action::MoveFocus(direction) => { let screen_instr = match direction { Direction::Left => ScreenInstruction::MoveFocusLeft(client_id), diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 0c073aed8b..e2807fb870 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -167,6 +167,7 @@ pub enum ScreenInstruction { SwitchFocus(ClientId), FocusNextPane(ClientId), FocusPreviousPane(ClientId), + FocusLastPane(ClientId), MoveFocusLeft(ClientId), MoveFocusLeftOrPreviousTab(ClientId), MoveFocusDown(ClientId), @@ -466,6 +467,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 @@ -3604,6 +3606,16 @@ pub(crate) fn screen_thread_main( screen.unblock_input()?; screen.log_and_report_session_state()?; }, + ScreenInstruction::FocusLastPane(client_id) => { + 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.unblock_input()?; + screen.log_and_report_session_state()?; + }, ScreenInstruction::MoveFocusLeft(client_id) => { active_tab_and_connected_client_id!( screen, diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 20e87f9dc8..d7b85d9c57 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -2676,6 +2676,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(); @@ -2990,6 +2996,16 @@ 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.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); diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 608fa2e76a..597f3e9501 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -507,6 +507,14 @@ pub fn focus_previous_pane() { unsafe { host_run_plugin_command() }; } +/// Change focus to the previously focused pane +pub fn focus_last_pane() { + let plugin_command = PluginCommand::FocusLastPane; + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + /// Change the focused pane in the specified direction pub fn move_focus(direction: Direction) { let plugin_command = PluginCommand::MoveFocus(direction); diff --git a/zellij-utils/assets/prost/api.action.rs b/zellij-utils/assets/prost/api.action.rs index b835236b01..ebd9a1bf18 100644 --- a/zellij-utils/assets/prost/api.action.rs +++ b/zellij-utils/assets/prost/api.action.rs @@ -476,6 +476,7 @@ pub enum ActionName { TogglePaneInGroup = 87, ToggleGroupMarking = 88, NewStackedPane = 89, + FocusLastPane = 90, } impl ActionName { /// String value of the enum field names used in the ProtoBuf definition. @@ -571,6 +572,7 @@ impl ActionName { ActionName::TogglePaneInGroup => "TogglePaneInGroup", ActionName::ToggleGroupMarking => "ToggleGroupMarking", ActionName::NewStackedPane => "NewStackedPane", + ActionName::FocusLastPane => "FocusLastPane", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -663,6 +665,7 @@ impl ActionName { "TogglePaneInGroup" => Some(Self::TogglePaneInGroup), "ToggleGroupMarking" => Some(Self::ToggleGroupMarking), "NewStackedPane" => Some(Self::NewStackedPane), + "FocusLastPane" => Some(Self::FocusLastPane), _ => None, } } diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs index e3c636263e..1348ef0a88 100644 --- a/zellij-utils/assets/prost/api.plugin_command.rs +++ b/zellij-utils/assets/prost/api.plugin_command.rs @@ -1024,6 +1024,7 @@ pub enum CommandName { InterceptKeyPresses = 143, ClearKeyPressesIntercepts = 144, ReplacePaneWithExistingPane = 155, + FocusLastPane = 156, } impl CommandName { /// String value of the enum field names used in the ProtoBuf definition. @@ -1178,6 +1179,7 @@ impl CommandName { CommandName::InterceptKeyPresses => "InterceptKeyPresses", CommandName::ClearKeyPressesIntercepts => "ClearKeyPressesIntercepts", CommandName::ReplacePaneWithExistingPane => "ReplacePaneWithExistingPane", + CommandName::FocusLastPane => "FocusLastPane", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -1329,6 +1331,7 @@ impl CommandName { "InterceptKeyPresses" => Some(Self::InterceptKeyPresses), "ClearKeyPressesIntercepts" => Some(Self::ClearKeyPressesIntercepts), "ReplacePaneWithExistingPane" => Some(Self::ReplacePaneWithExistingPane), + "FocusLastPane" => Some(Self::FocusLastPane), _ => None, } } diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 164f59996d..706218a4f3 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -483,6 +483,8 @@ pub enum CliAction { FocusNextPane, /// Change focus to the previous pane FocusPreviousPane, + /// Change focus to the last focused frame + FocusLastPane, /// Move the focused pane in the specified direction. [right|left|up|down] MoveFocus { direction: Direction, diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index 0520e15d68..a9cea30ebf 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -2351,6 +2351,7 @@ pub enum PluginCommand { ResizeWithDirection(ResizeStrategy), FocusNextPane, FocusPreviousPane, + FocusLastPane, MoveFocus(Direction), MoveFocusOrTab(Direction), Detach, diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index b4082cd112..91c5b45b18 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -242,6 +242,7 @@ pub enum ScreenContext { SwitchFocus, FocusNextPane, FocusPreviousPane, + FocusLastPane, FocusPaneAt, MoveFocusLeft, MoveFocusLeftOrPreviousTab, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index 38c46833cb..c7e4f615cf 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -115,6 +115,8 @@ pub enum Action { /// Switch focus to next pane in specified direction. FocusNextPane, FocusPreviousPane, + /// Switch focus to the last focused pane. + FocusLastPane, /// Move the focus pane in specified direction. SwitchFocus, MoveFocus(Direction), @@ -324,6 +326,7 @@ impl Action { CliAction::Resize { resize, direction } => Ok(vec![Action::Resize(resize, direction)]), CliAction::FocusNextPane => Ok(vec![Action::FocusNextPane]), CliAction::FocusPreviousPane => Ok(vec![Action::FocusPreviousPane]), + CliAction::FocusLastPane => Ok(vec![Action::FocusLastPane]), CliAction::MoveFocus { direction } => Ok(vec![Action::MoveFocus(direction)]), CliAction::MoveFocusOrTab { direction } => Ok(vec![Action::MoveFocusOrTab(direction)]), CliAction::MovePane { direction } => Ok(vec![Action::MovePane(direction)]), diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 724f52be0f..3746913195 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -46,6 +46,7 @@ macro_rules! parse_kdl_action_arguments { "Quit" => Ok(Action::Quit), "FocusNextPane" => Ok(Action::FocusNextPane), "FocusPreviousPane" => Ok(Action::FocusPreviousPane), + "FocusLastPane" => Ok(Action::FocusLastPane), "SwitchFocus" => Ok(Action::SwitchFocus), "EditScrollback" => Ok(Action::EditScrollback), "ScrollUp" => Ok(Action::ScrollUp), @@ -617,6 +618,7 @@ impl Action { }, Action::FocusNextPane => Some(KdlNode::new("FocusNextPane")), Action::FocusPreviousPane => Some(KdlNode::new("FocusPreviousPane")), + Action::FocusLastPane => Some(KdlNode::new("FocusLastPane")), Action::SwitchFocus => Some(KdlNode::new("SwitchFocus")), Action::MoveFocus(direction) => { let mut node = KdlNode::new("MoveFocus"); @@ -1339,6 +1341,9 @@ impl TryFrom<(&KdlNode, &Options)> for Action { "FocusPreviousPane" => { parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) }, + "FocusLastPane" => { + parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) + }, "SwitchFocus" => parse_kdl_action_arguments!(action_name, action_arguments, kdl_action), "EditScrollback" => { parse_kdl_action_arguments!(action_name, action_arguments, kdl_action) @@ -5760,6 +5765,7 @@ fn keybinds_to_string_with_all_actions() { config_key_2 "config_value_2"; }; } + bind "Ctrl Alt k" { FocusLastPane; } } }"#; let document: KdlDocument = fake_config.parse().unwrap(); diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_all_actions.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_all_actions.snap index 43e85faa5c..b0119a63da 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_all_actions.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_all_actions.snap @@ -1,6 +1,5 @@ --- source: zellij-utils/src/kdl/mod.rs -assertion_line: 2731 expression: serialized.to_string() --- keybinds clear-defaults=true { @@ -47,6 +46,7 @@ keybinds clear-defaults=true { } bind "Alt j" { GoToPreviousTab; } bind "Ctrl k" { MovePane "right"; } + bind "Ctrl Alt k" { FocusLastPane; } bind "Alt k" { CloseTab; } bind "Ctrl l" { MovePaneBackwards; } bind "Alt l" { GoToTab 1; } @@ -127,4 +127,3 @@ keybinds clear-defaults=true { bind "Alt z" { SearchInput 0; } } } - diff --git a/zellij-utils/src/plugin_api/action.proto b/zellij-utils/src/plugin_api/action.proto index 5bdd34e920..a8c2a86cc6 100644 --- a/zellij-utils/src/plugin_api/action.proto +++ b/zellij-utils/src/plugin_api/action.proto @@ -244,6 +244,7 @@ enum ActionName { TogglePaneInGroup = 87; ToggleGroupMarking = 88; NewStackedPane = 89; + FocusLastPane = 90; } message Position { diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs index d018ea1b1d..4e1dc31b7a 100644 --- a/zellij-utils/src/plugin_api/action.rs +++ b/zellij-utils/src/plugin_api/action.rs @@ -91,6 +91,10 @@ impl TryFrom for Action { Some(_) => Err("FocusPreviousPane should not have a payload"), None => Ok(Action::FocusPreviousPane), }, + Some(ProtobufActionName::FocusLastPane) => match protobuf_action.optional_payload { + Some(_) => Err("FocusLastPane should not have a payload"), + None => Ok(Action::FocusLastPane), + }, Some(ProtobufActionName::SwitchFocus) => match protobuf_action.optional_payload { Some(_) => Err("SwitchFocus should not have a payload"), None => Ok(Action::SwitchFocus), @@ -790,6 +794,10 @@ impl TryFrom for ProtobufAction { name: ProtobufActionName::FocusPreviousPane as i32, optional_payload: None, }), + Action::FocusLastPane => Ok(ProtobufAction { + name: ProtobufActionName::FocusLastPane as i32, + optional_payload: None, + }), Action::SwitchFocus => Ok(ProtobufAction { name: ProtobufActionName::SwitchFocus as i32, optional_payload: None, diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto index b1797214f6..bb94aa1659 100644 --- a/zellij-utils/src/plugin_api/plugin_command.proto +++ b/zellij-utils/src/plugin_api/plugin_command.proto @@ -159,6 +159,7 @@ enum CommandName { InterceptKeyPresses = 143; ClearKeyPressesIntercepts = 144; ReplacePaneWithExistingPane = 155; + FocusLastPane = 156; } message PluginCommand { diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs index 296a8a7423..ed338ade5c 100644 --- a/zellij-utils/src/plugin_api/plugin_command.rs +++ b/zellij-utils/src/plugin_api/plugin_command.rs @@ -520,6 +520,12 @@ impl TryFrom for PluginCommand { } Ok(PluginCommand::FocusPreviousPane) }, + Some(CommandName::FocusLastPane) => { + if protobuf_plugin_command.payload.is_some() { + return Err("FocusLastPane should not have a payload"); + } + Ok(PluginCommand::FocusLastPane) + }, Some(CommandName::MoveFocus) => match protobuf_plugin_command.payload { Some(Payload::MoveFocusPayload(move_payload)) => match move_payload.direction { Some(direction) => Ok(PluginCommand::MoveFocus(direction.try_into()?)), @@ -1918,6 +1924,10 @@ impl TryFrom for ProtobufPluginCommand { name: CommandName::FocusPreviousPane as i32, payload: None, }), + PluginCommand::FocusLastPane => Ok(ProtobufPluginCommand { + name: CommandName::FocusLastPane as i32, + payload: None, + }), PluginCommand::MoveFocus(direction) => Ok(ProtobufPluginCommand { name: CommandName::MoveFocus as i32, payload: Some(Payload::MoveFocusPayload(MovePayload { From 19f99fdefd014d723d54ed822821ab9a30244ae7 Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Sat, 21 Jun 2025 21:28:53 +0200 Subject: [PATCH 2/6] add tests --- .../fixture-plugin-for-tests/src/main.rs | 1 + .../src/plugins/unit/plugin_tests.rs | 73 ++++++++++++++++ ...tests__focus_last_pane_plugin_command.snap | 9 ++ ...ration_tests__focus_last_stacked_pane.snap | 24 ++++++ .../src/tab/unit/tab_integration_tests.rs | 83 ++++++++++++++++++ zellij-server/src/tab/unit/tab_tests.rs | 84 +++++++++++++++++++ zellij-server/src/unit/screen_tests.rs | 45 ++++++++++ ...ts__send_cli_focus_last_pane_action-2.snap | 5 ++ ...ts__send_cli_focus_last_pane_action-3.snap | 5 ++ ...ts__send_cli_focus_last_pane_action-4.snap | 5 ++ ...ts__send_cli_focus_last_pane_action-5.snap | 5 ++ ...ts__send_cli_focus_last_pane_action-6.snap | 5 ++ ...ests__send_cli_focus_last_pane_action.snap | 5 ++ 13 files changed, 349 insertions(+) create mode 100644 zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_last_pane_plugin_command.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__focus_last_stacked_pane.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-2.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-3.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-4.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-5.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-6.snap create mode 100644 zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action.snap diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 7f66fa4e24..c99299d720 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -529,6 +529,7 @@ impl ZellijPlugin for State { BareKey::Char('z') if key.has_modifiers(&[KeyModifier::Alt]) => { list_clients(); }, + BareKey::Char('0') if key.has_modifiers(&[KeyModifier::Alt]) => focus_last_pane(), _ => {}, }, Event::CustomMessage(message, payload) => { diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 276461d905..1437bfbc7f 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -1829,6 +1829,79 @@ pub fn focus_previous_pane_plugin_command() { assert_snapshot!(format!("{:#?}", new_tab_event)); } +#[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)); + 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, + plugin_title, + run_plugin, + Some(tab_index), + None, + client_id, + size, + None, + false, + 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('0')).with_alt_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() { 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 0000000000..5e09b50c55 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_last_pane_plugin_command.snap @@ -0,0 +1,9 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + FocusLastPane( + 1, + ), +) 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 0000000000..190ec39026 --- /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/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs index 8f5ff185c6..f0c4b88df8 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -5520,6 +5520,89 @@ fn move_focus_down_into_stacked_panes() { assert_snapshot!(snapshot); } +#[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, + None, + None, + false, + true, + Some(client_id), + ) + .unwrap(); + tab.new_pane( + new_pane_id_2, + None, + None, + None, + None, + false, + true, + Some(client_id), + ) + .unwrap(); + tab.new_pane( + new_pane_id_3, + None, + None, + None, + None, + false, + true, + Some(client_id), + ) + .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).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 { diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index bca69529d8..852adae86c 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -1197,6 +1197,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, + None, + None, + false, + true, + Some(1), + ) + .unwrap(); + active_tab + .new_pane( + PaneId::Terminal(2), + None, + None, + None, + None, + false, + true, + Some(1), + ) + .unwrap(); + active_tab + .new_pane( + PaneId::Terminal(3), + None, + None, + None, + None, + false, + true, + Some(1), + ) + .unwrap(); + active_tab + .new_pane( + PaneId::Terminal(4), + None, + None, + None, + None, + false, + true, + Some(1), + ) + .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() { // ┌───────────┐ ┌───────────┐ diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index a116486479..9745a64c12 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -1881,6 +1881,51 @@ pub fn send_cli_focus_previous_pane_action() { assert_snapshot!(format!("{}", snapshot_count)); } +#[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 }; 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 0000000000..4078f02744 --- /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 0000000000..7c6ee1f357 --- /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 0000000000..4078f02744 --- /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 0000000000..7c6ee1f357 --- /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 0000000000..84b05c9ceb --- /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 0000000000..e627107fb4 --- /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)) From 16bbe35e059bef67998cc493775b8fd1a1a06d28 Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Sat, 21 Jun 2025 21:22:21 +0200 Subject: [PATCH 3/6] add client id parameter to `add_floating_pane` --- zellij-server/src/screen.rs | 6 ++++-- zellij-server/src/tab/mod.rs | 17 +++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index e2807fb870..5b2895d29c 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -2204,6 +2204,7 @@ impl Screen { pane_id, None, true, + Some(client_id) )?; } else { new_active_tab.hide_floating_panes(); @@ -2317,7 +2318,7 @@ impl Screen { let (mut tiled_panes_layout, mut floating_panes_layout) = default_layout.new_tab(); if pane_to_break_is_floating { tab.show_floating_panes(); - tab.add_floating_pane(active_pane, active_pane_id, None, true)?; + tab.add_floating_pane(active_pane, active_pane_id, None, true, Some(client_id))?; if let Some(already_running_layout) = floating_panes_layout .iter_mut() .find(|i| i.run == active_pane_run_instruction) @@ -2453,7 +2454,7 @@ 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(active_pane, active_pane_id, Some(client_id))?; @@ -2534,6 +2535,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 diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index d7b85d9c57..f251fc75b8 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -1172,7 +1172,7 @@ 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(()) @@ -1208,7 +1208,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(()) @@ -1415,13 +1415,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, 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, client_id) } @@ -1606,7 +1606,7 @@ 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( @@ -4922,7 +4922,7 @@ impl Tab { pane.1.set_selectable(true); if should_float { self.show_floating_panes(); - self.add_floating_pane(pane.1, pane_id, None, true) + self.add_floating_pane(pane.1, pane_id, None, true, Some(client_id)) } else { self.hide_floating_panes(); self.add_tiled_pane(pane.1, pane_id, Some(client_id)) @@ -4935,7 +4935,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); }, @@ -4979,6 +4979,7 @@ impl Tab { pane_id: PaneId, floating_pane_coordinates: Option, should_focus_new_pane: bool, + client_id: Option, ) -> 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() { @@ -5350,7 +5351,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)?; } } } From aab3a06918bcbb27bf6f2403368f796bedfae43a Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Sat, 21 Jun 2025 21:25:03 +0200 Subject: [PATCH 4/6] add action for floating panes --- zellij-server/src/panes/floating_panes/mod.rs | 12 ++++++++++++ zellij-server/src/tab/mod.rs | 11 +++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index d8ce0c7ca8..807a57e523 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -635,6 +635,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); @@ -829,6 +836,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/tab/mod.rs b/zellij-server/src/tab/mod.rs index f251fc75b8..46e81bb5c7 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -3000,7 +3000,10 @@ impl Tab { if !self.has_selectable_panes() { return; } - if self.tiled_panes.fullscreen_is_active() { + 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; } @@ -4999,7 +5002,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() { From 2e8e721cbb97a485f5d0edda8153d745a735823d Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Sat, 21 Jun 2025 21:38:47 +0200 Subject: [PATCH 5/6] add tests for floating panes --- ...move_floating_pane_focus_to_last_pane.snap | 24 +++++ .../src/tab/unit/tab_integration_tests.rs | 102 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_to_last_pane.snap 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 0000000000..1f1604ef55 --- /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 f0c4b88df8..c87aa9e474 100644 --- a/zellij-server/src/tab/unit/tab_integration_tests.rs +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -2300,6 +2300,108 @@ fn move_floating_pane_focus_with_mouse() { assert_snapshot!(snapshot); } +#[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).unwrap(); + tab.new_pane( + new_pane_id_1, + None, + None, + None, + None, + false, + true, + Some(client_id), + ) + .unwrap(); + tab.new_pane( + new_pane_id_2, + None, + None, + None, + None, + false, + true, + Some(client_id), + ) + .unwrap(); + tab.new_pane( + new_pane_id_3, + None, + None, + None, + None, + false, + true, + Some(client_id), + ) + .unwrap(); + tab.new_pane( + new_pane_id_4, + None, + None, + None, + None, + false, + true, + Some(client_id), + ) + .unwrap(); + tab.new_pane( + new_pane_id_5, + None, + None, + None, + None, + false, + true, + Some(client_id), + ) + .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).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 { From 14d8b7178b2ff46e6c18f204e62d76c0e81c5cbc Mon Sep 17 00:00:00 2001 From: Dawid Pietrykowski Date: Sat, 21 Jun 2025 21:46:57 +0200 Subject: [PATCH 6/6] add keybinds to configs --- example/alt-centered-config.kdl | 1 + example/config.kdl | 2 ++ example/default.kdl | 2 ++ 3 files changed, 5 insertions(+) diff --git a/example/alt-centered-config.kdl b/example/alt-centered-config.kdl index 26a105cb8e..b9803880f8 100644 --- a/example/alt-centered-config.kdl +++ b/example/alt-centered-config.kdl @@ -30,6 +30,7 @@ keybinds { bind "Alt k" { MoveFocus "Up"; } bind "Alt +" { Resize "Increase"; } bind "Alt -" { Resize "Decrease"; } + bind "Alt \\" { FocusLastPane; } } pane clear-defaults=true { bind "Enter" "Esc" "Space" { SwitchToMode "normal"; } diff --git a/example/config.kdl b/example/config.kdl index 44f56a8fd1..cbfb7a63b6 100644 --- a/example/config.kdl +++ b/example/config.kdl @@ -22,6 +22,7 @@ keybinds { bind "l" "Right" { MoveFocus "Right"; } bind "j" "Down" { MoveFocus "Down"; } bind "k" "Up" { MoveFocus "Up"; } + bind "\\" { FocusLastPane; } bind "p" { SwitchFocus; } bind "n" { NewPane; SwitchToMode "Normal"; } bind "d" { NewPane "Down"; SwitchToMode "Normal"; } @@ -125,6 +126,7 @@ keybinds { bind "j" { MoveFocus "Down"; SwitchToMode "Normal"; } bind "k" { MoveFocus "Up"; SwitchToMode "Normal"; } bind "o" { FocusNextPane; } + bind ";" { FocusLastPane; } bind "d" { Detach; } bind "x" { CloseFocus; SwitchToMode "Normal"; } } diff --git a/example/default.kdl b/example/default.kdl index 983d6b66cf..f516a978bc 100644 --- a/example/default.kdl +++ b/example/default.kdl @@ -169,6 +169,7 @@ keybinds { bind "j" { MoveFocus "Down"; SwitchToMode "Normal"; } bind "k" { MoveFocus "Up"; SwitchToMode "Normal"; } bind "o" { FocusNextPane; } + bind ";" { FocusLastPane; } bind "d" { Detach; } bind "Space" { NextSwapLayout; } bind "x" { CloseFocus; SwitchToMode "Normal"; } @@ -188,6 +189,7 @@ keybinds { bind "Alt -" { Resize "Decrease"; } bind "Alt [" { PreviousSwapLayout; } bind "Alt ]" { NextSwapLayout; } + bind "Alt \\" { FocusLastPane; } } shared_except "normal" "locked" { bind "Enter" "Esc" { SwitchToMode "Normal"; }