aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-server
diff options
context:
space:
mode:
authorDawidPietrykowski <53954695+DawidPietrykowski@users.noreply.github.com>2026-06-25 18:12:51 +0200
committerGitHub <noreply@github.com>2026-06-25 18:12:51 +0200
commitebd731b0ff12737a6e2160a255280534ef3991bd (patch)
tree4313963a269205391aa519fd090ea1de2b7cc4af /zellij-server
parenttests: add more integration tests (#5308) (diff)
downloadsl0ppij-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')
-rw-r--r--zellij-server/src/panes/active_panes.rs8
-rw-r--r--zellij-server/src/panes/floating_panes/mod.rs12
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs58
-rw-r--r--zellij-server/src/plugins/unit/plugin_tests.rs80
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__focus_last_pane_plugin_command.snap21
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs7
-rw-r--r--zellij-server/src/route.rs8
-rw-r--r--zellij-server/src/screen.rs25
-rw-r--r--zellij-server/src/tab/mod.rs57
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__focus_last_stacked_pane.snap24
-rw-r--r--zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_to_last_pane.snap24
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs185
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs84
-rw-r--r--zellij-server/src/unit/screen_tests.rs49
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-2.snap5
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-3.snap5
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-4.snap5
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-5.snap5
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action-6.snap5
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_focus_last_pane_action.snap5
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))