aboutsummaryrefslogtreecommitdiffstats
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
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>
-rw-r--r--CHANGELOG.md1
-rw-r--r--default-plugins/fixture-plugin-for-tests/src/main.rs5
-rw-r--r--docs/MANPAGE.md1
-rw-r--r--example/alt-centered-config.kdl1
-rw-r--r--example/config.kdl2
-rw-r--r--example/default.kdl2
-rw-r--r--zellij-client/src/old_config_converter/old_config.rs2
-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
-rw-r--r--zellij-tile/src/shim.rs8
-rw-r--r--zellij-utils/assets/prost/api.action.rs3
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs3
-rw-r--r--zellij-utils/assets/prost_ipc/client_server_contract.rs8
-rw-r--r--zellij-utils/src/cli.rs2
-rw-r--r--zellij-utils/src/client_server_contract/common_types.proto2
-rw-r--r--zellij-utils/src/data.rs1
-rw-r--r--zellij-utils/src/errors.rs1
-rw-r--r--zellij-utils/src/input/actions.rs3
-rw-r--r--zellij-utils/src/ipc/protobuf_conversion.rs5
-rw-r--r--zellij-utils/src/kdl/mod.rs6
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__keybinds_to_string_with_all_actions.snap3
-rw-r--r--zellij-utils/src/plugin_api/action.proto1
-rw-r--r--zellij-utils/src/plugin_api/action.rs8
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto1
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs10
43 files changed, 731 insertions, 20 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bcc9c33b..3aac2d13 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* feat: PWA support for the web client (manifest + icons + iOS meta tags) so the page can be installed as a standalone app (https://github.com/zellij-org/zellij/pull/5184)
* feat: mobile UI (https://github.com/zellij-org/zellij/pull/5241)
* chore: new integration testing infra (https://github.com/zellij-org/zellij/pull/5269)
+* feat: FocusLastPane bindable action (https://github.com/zellij-org/zellij/pull/4241)
## [0.44.3] - 2026-05-13
* fix(windows): bump windows-sys to 0.59 to align manifest with code, fixing source builds via `cargo install`/`cargo binstall` (https://github.com/zellij-org/zellij/pull/5139)
diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs
index b8b4f3ce..4885054c 100644
--- a/default-plugins/fixture-plugin-for-tests/src/main.rs
+++ b/default-plugins/fixture-plugin-for-tests/src/main.rs
@@ -935,6 +935,11 @@ impl ZellijPlugin for State {
);
self.explicit_string_to_render = Some(format!("Override layout command sent"));
},
+ BareKey::Char('l')
+ if key.has_only_modifiers(&[KeyModifier::Ctrl, KeyModifier::Alt]) =>
+ {
+ focus_last_pane()
+ },
_ => {},
},
Event::CustomMessage(message, payload) => {
diff --git a/docs/MANPAGE.md b/docs/MANPAGE.md
index 1d490c80..8e8a76bb 100644
--- a/docs/MANPAGE.md
+++ b/docs/MANPAGE.md
@@ -151,6 +151,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: <Direction\>__ - moves focus in the specified direction (Left,
diff --git a/example/alt-centered-config.kdl b/example/alt-centered-config.kdl
index 26a105cb..b9803880 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 44f56a8f..cbfb7a63 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 61aa4bb8..f1d9c50b 100644
--- a/example/default.kdl
+++ b/example/default.kdl
@@ -177,6 +177,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"; }
@@ -198,6 +199,7 @@ keybinds {
bind "Alt ]" { NextSwapLayout; }
bind "Alt p" { TogglePaneInGroup; }
bind "Alt Shift p" { ToggleGroupMarking; }
+ bind "Alt \\" { FocusLastPane; }
}
shared_except "normal" "locked" {
bind "Enter" "Esc" { SwitchToMode "Normal"; }
diff --git a/zellij-client/src/old_config_converter/old_config.rs b/zellij-client/src/old_config_converter/old_config.rs
index b5083c17..dc43debb 100644
--- a/zellij-client/src/old_config_converter/old_config.rs
+++ b/zellij-client/src/old_config_converter/old_config.rs
@@ -950,6 +950,7 @@ enum OldAction {
Resize(OldResizeDirection),
FocusNextPane,
FocusPreviousPane,
+ FocusLastPane,
SwitchFocus,
MoveFocus(OldDirection),
MoveFocusOrTab(OldDirection),
@@ -1021,6 +1022,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 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))
diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs
index a7c1589f..54431151 100644
--- a/zellij-tile/src/shim.rs
+++ b/zellij-tile/src/shim.rs
@@ -1137,6 +1137,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 8d331b60..31325096 100644
--- a/zellij-utils/assets/prost/api.action.rs
+++ b/zellij-utils/assets/prost/api.action.rs
@@ -1124,6 +1124,7 @@ pub enum ActionName {
SetDarkTheme = 101,
SetLightTheme = 102,
ToggleTheme = 103,
+ FocusLastPane = 104,
}
impl ActionName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -1233,6 +1234,7 @@ impl ActionName {
ActionName::SetDarkTheme => "SetDarkTheme",
ActionName::SetLightTheme => "SetLightTheme",
ActionName::ToggleTheme => "ToggleTheme",
+ ActionName::FocusLastPane => "FocusLastPane",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -1339,6 +1341,7 @@ impl ActionName {
"SetDarkTheme" => Some(Self::SetDarkTheme),
"SetLightTheme" => Some(Self::SetLightTheme),
"ToggleTheme" => Some(Self::ToggleTheme),
+ "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 b43bd004..4cef814a 100644
--- a/zellij-utils/assets/prost/api.plugin_command.rs
+++ b/zellij-utils/assets/prost/api.plugin_command.rs
@@ -2229,6 +2229,7 @@ pub enum CommandName {
NewTabUnfocused = 220,
NewTiledPaneInTab = 221,
ExitMobileMode = 222,
+ FocusLastPane = 223,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -2437,6 +2438,7 @@ impl CommandName {
CommandName::NewTabUnfocused => "NewTabUnfocused",
CommandName::NewTiledPaneInTab => "NewTiledPaneInTab",
CommandName::ExitMobileMode => "ExitMobileMode",
+ CommandName::FocusLastPane => "FocusLastPane",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -2642,6 +2644,7 @@ impl CommandName {
"NewTabUnfocused" => Some(Self::NewTabUnfocused),
"NewTiledPaneInTab" => Some(Self::NewTiledPaneInTab),
"ExitMobileMode" => Some(Self::ExitMobileMode),
+ "FocusLastPane" => Some(Self::FocusLastPane),
_ => None,
}
}
diff --git a/zellij-utils/assets/prost_ipc/client_server_contract.rs b/zellij-utils/assets/prost_ipc/client_server_contract.rs
index 4ef94968..e08496e7 100644
--- a/zellij-utils/assets/prost_ipc/client_server_contract.rs
+++ b/zellij-utils/assets/prost_ipc/client_server_contract.rs
@@ -119,7 +119,7 @@ pub struct RgbColor {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Action {
- #[prost(oneof="action::ActionType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140")]
+ #[prost(oneof="action::ActionType", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141")]
pub action_type: ::core::option::Option<action::ActionType>,
}
/// Nested message and enum types in `Action`.
@@ -409,6 +409,8 @@ pub mod action {
ToggleTheme(super::ToggleThemeAction),
#[prost(message, tag="140")]
ToggleMobileMode(super::ToggleMobileModeAction),
+ #[prost(message, tag="141")]
+ FocusLastPane(super::FocusLastPaneAction),
}
}
// Action message definitions (all 92 variants)
@@ -428,6 +430,10 @@ pub struct FocusPreviousPaneAction {
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct FocusLastPaneAction {
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SwitchFocusAction {
}
#[allow(clippy::derive_partial_eq_without_eq)]
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs
index 5b169170..a00c4d05 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -765,6 +765,8 @@ pub enum CliAction {
/// The pane_id of the pane, eg. terminal_1, plugin_2 or 3
pane_id: String,
},
+ /// 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/client_server_contract/common_types.proto b/zellij-utils/src/client_server_contract/common_types.proto
index 1dfc3994..718cc279 100644
--- a/zellij-utils/src/client_server_contract/common_types.proto
+++ b/zellij-utils/src/client_server_contract/common_types.proto
@@ -278,6 +278,7 @@ message Action {
SetLightThemeAction set_light_theme = 138;
ToggleThemeAction toggle_theme = 139;
ToggleMobileModeAction toggle_mobile_mode = 140;
+ FocusLastPaneAction focus_last_pane = 141;
}
}
@@ -287,6 +288,7 @@ message Action {
message QuitAction {}
message FocusNextPaneAction {}
message FocusPreviousPaneAction {}
+message FocusLastPaneAction {}
message SwitchFocusAction {}
message MovePaneBackwardsAction {}
message ClearScreenAction {}
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index 16ff560e..c11f777f 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -3384,6 +3384,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 8e4bc66c..c9e8e25f 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -246,6 +246,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 cf5fc448..40158b8b 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -158,6 +158,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 {
@@ -828,6 +830,7 @@ impl Action {
))?;
Ok(vec![Action::FocusPaneByPaneId { pane_id }])
},
+ CliAction::FocusLastPane => Ok(vec![Action::FocusLastPane]),
CliAction::MoveFocus { direction } => Ok(vec![Action::MoveFocus { direction }]),
CliAction::MoveFocusOrTab { direction } => {
Ok(vec![Action::MoveFocusOrTab { direction }])
diff --git a/zellij-utils/src/ipc/protobuf_conversion.rs b/zellij-utils/src/ipc/protobuf_conversion.rs
index d1bfb7d4..c4941676 100644
--- a/zellij-utils/src/ipc/protobuf_conversion.rs
+++ b/zellij-utils/src/ipc/protobuf_conversion.rs
@@ -915,6 +915,7 @@ impl From<crate::input::actions::Action>
EditFileAction,
EditScrollbackAction,
EditScrollbackByPaneIdAction,
+ FocusLastPaneAction,
FocusNextPaneAction,
FocusPaneByPaneIdAction,
FocusPluginPaneWithIdAction,
@@ -1090,6 +1091,9 @@ impl From<crate::input::actions::Action>
crate::input::actions::Action::FocusPreviousPane => {
ActionType::FocusPreviousPane(FocusPreviousPaneAction {})
},
+ crate::input::actions::Action::FocusLastPane => {
+ ActionType::FocusLastPane(FocusLastPaneAction {})
+ },
crate::input::actions::Action::SwitchFocus => {
ActionType::SwitchFocus(SwitchFocusAction {})
},
@@ -1961,6 +1965,7 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Action>
ActionType::FocusPreviousPane(_) => {
Ok(crate::input::actions::Action::FocusPreviousPane)
},
+ ActionType::FocusLastPane(_) => Ok(crate::input::actions::Action::FocusLastPane),
ActionType::SwitchFocus(_) => Ok(crate::input::actions::Action::SwitchFocus),
ActionType::MoveFocus(move_focus_action) => {
Ok(crate::input::actions::Action::MoveFocus {
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index 0164ef92..14695f68 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -48,6 +48,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 { ansi: false }),
"ScrollUp" => Ok(Action::ScrollUp),
@@ -660,6 +661,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");
@@ -1512,6 +1514,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" => {
let ansi = crate::kdl_get_bool_property_or_child_value!(kdl_action, "ansi")
@@ -6729,6 +6734,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 43e85faa..b0119a63 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 7eedf393..68c859b8 100644
--- a/zellij-utils/src/plugin_api/action.proto
+++ b/zellij-utils/src/plugin_api/action.proto
@@ -493,6 +493,7 @@ enum ActionName {
SetDarkTheme = 101;
SetLightTheme = 102;
ToggleTheme = 103;
+ FocusLastPane = 104;
}
message Position {
diff --git a/zellij-utils/src/plugin_api/action.rs b/zellij-utils/src/plugin_api/action.rs
index 34b3adce..f4e35d84 100644
--- a/zellij-utils/src/plugin_api/action.rs
+++ b/zellij-utils/src/plugin_api/action.rs
@@ -168,6 +168,10 @@ impl TryFrom<ProtobufAction> 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),
@@ -1173,6 +1177,10 @@ impl TryFrom<Action> 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 c65bcbfd..82f4d835 100644
--- a/zellij-utils/src/plugin_api/plugin_command.proto
+++ b/zellij-utils/src/plugin_api/plugin_command.proto
@@ -213,6 +213,7 @@ enum CommandName {
NewTabUnfocused = 220;
NewTiledPaneInTab = 221;
ExitMobileMode = 222;
+ FocusLastPane = 223;
}
message PluginCommand {
diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs
index 42b0774f..546c3586 100644
--- a/zellij-utils/src/plugin_api/plugin_command.rs
+++ b/zellij-utils/src/plugin_api/plugin_command.rs
@@ -980,6 +980,12 @@ impl TryFrom<ProtobufPluginCommand> 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()?)),
@@ -3005,6 +3011,10 @@ impl TryFrom<PluginCommand> 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 {