aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-26 14:48:57 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-26 14:50:13 +0300
commit57c880f0ebaa5011abc90f4e2fd4f84fc072ce35 (patch)
tree9f662b19cec2e9c9e50e87eb5e96def9e8ae9822 /src/app/input
parent9eec3029e13b791b8a4954e38d5bb7db6e2ff68f (diff)
feat: add global last-pane navigation
refs #287
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/mod.rs8
-rw-r--r--src/app/input/mouse.rs40
-rw-r--r--src/app/input/navigate.rs62
3 files changed, 87 insertions, 23 deletions
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs
index 0f0b3568..e145e628 100644
--- a/src/app/input/mod.rs
+++ b/src/app/input/mod.rs
@@ -367,7 +367,11 @@ impl AppState {
follow_cwd,
));
- if let Some(ws) = self.active.and_then(|i| self.workspaces.get_mut(i)) {
+ let previous_focus = self.current_pane_focus_target();
+ if let Some(ws_idx) = self.active {
+ let Some(ws) = self.workspaces.get_mut(ws_idx) else {
+ return;
+ };
if let Ok(new_pane) = ws.split_focused(
direction,
new_rows,
@@ -381,7 +385,7 @@ impl AppState {
terminal_runtimes.insert(new_pane.terminal.id.clone(), new_pane.runtime);
self.terminals
.insert(new_pane.terminal.id.clone(), new_pane.terminal);
- ws.layout.focus_pane(new_id);
+ self.record_pane_focus_change(previous_focus, ws_idx, new_id);
self.mark_session_dirty();
self.mode = Mode::Terminal;
}
diff --git a/src/app/input/mouse.rs b/src/app/input/mouse.rs
index 8adfa55b..6db1a4a9 100644
--- a/src/app/input/mouse.rs
+++ b/src/app/input/mouse.rs
@@ -455,12 +455,10 @@ impl AppState {
return None;
}
- if let Some((ws_idx, tab_idx, pane_id)) =
+ if let Some((ws_idx, _tab_idx, pane_id)) =
self.collapsed_agent_detail_target_at(mouse.row)
{
- self.switch_workspace(ws_idx);
- self.switch_tab(tab_idx);
- self.focus_pane(pane_id);
+ self.focus_pane_in_workspace(ws_idx, pane_id);
self.mode = Mode::Terminal;
}
return None;
@@ -550,11 +548,10 @@ impl AppState {
return None;
}
- if let Some((ws_idx, tab_idx, pane_id)) = self.agent_detail_target_at(mouse.row)
+ if let Some((ws_idx, _tab_idx, pane_id)) =
+ self.agent_detail_target_at(mouse.row)
{
- self.switch_workspace(ws_idx);
- self.switch_tab(tab_idx);
- self.focus_pane(pane_id);
+ self.focus_pane_in_workspace(ws_idx, pane_id);
self.mode = Mode::Terminal;
return None;
}
@@ -1027,12 +1024,10 @@ impl AppState {
}
Some(crate::ui::MobileSwitcherTarget::Agent {
ws_idx,
- tab_idx,
+ tab_idx: _,
pane_id,
}) => {
- self.switch_workspace(ws_idx);
- self.switch_tab(tab_idx);
- self.focus_pane(pane_id);
+ self.focus_pane_in_workspace(ws_idx, pane_id);
self.mode = Mode::Terminal;
}
Some(crate::ui::MobileSwitcherTarget::Menu(action_idx)) => {
@@ -1264,11 +1259,8 @@ impl AppState {
}
pub(super) fn focus_pane(&mut self, pane_id: crate::layout::PaneId) {
- if let Some(ws) = self.active.and_then(|i| self.workspaces.get_mut(i)) {
- if ws.layout.focused() != pane_id {
- ws.layout.focus_pane(pane_id);
- self.mark_session_dirty();
- }
+ if let Some(ws_idx) = self.active {
+ self.focus_pane_in_workspace(ws_idx, pane_id);
}
}
@@ -1290,13 +1282,11 @@ impl AppState {
else {
return;
};
- let Some(tab_idx) = self.workspaces[ws_idx].find_tab_index_for_pane(target.pane_id) else {
+ let Some(_tab_idx) = self.workspaces[ws_idx].find_tab_index_for_pane(target.pane_id) else {
return;
};
- self.switch_workspace(ws_idx);
- self.switch_tab(tab_idx);
- self.focus_pane(target.pane_id);
+ self.focus_pane_in_workspace(ws_idx, target.pane_id);
self.toast = None;
self.mode = Mode::Terminal;
}
@@ -1738,6 +1728,14 @@ mod tests {
assert_eq!(app.state.workspaces[1].focused_pane_id(), Some(target_pane));
assert!(app.state.toast.is_none());
assert_eq!(app.state.mode, Mode::Terminal);
+
+ app.state.last_pane();
+
+ assert_eq!(app.state.active, Some(0));
+ assert_eq!(
+ app.state.workspaces[0].focused_pane_id(),
+ Some(app.state.workspaces[0].tabs[0].root_pane)
+ );
}
#[test]
diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs
index 108fed8b..3b8435df 100644
--- a/src/app/input/navigate.rs
+++ b/src/app/input/navigate.rs
@@ -285,6 +285,7 @@ impl App {
let Some(ws_idx) = self.state.active else {
return Err(std::io::Error::other("no active workspace"));
};
+ let previous_focus_target = self.state.current_pane_focus_target();
let (rows, cols) = self.state.estimate_pane_size();
let new_rows = rows.max(4);
let new_cols = cols.max(10);
@@ -323,6 +324,13 @@ impl App {
self.state
.terminals
.insert(new_pane.terminal.id.clone(), new_pane.terminal);
+ let new_focus_target = crate::app::state::PaneFocusTarget {
+ workspace_id: ws.id.clone(),
+ pane_id: new_pane_id,
+ };
+ if previous_focus_target.as_ref() != Some(&new_focus_target) {
+ self.state.previous_pane_focus = previous_focus_target;
+ }
ws.active_tab_mut()
.expect("workspace must have an active tab")
.layout
@@ -499,6 +507,7 @@ pub(crate) enum NavigateAction {
ToggleSidebar,
CyclePaneNext,
CyclePanePrevious,
+ LastPane,
Help,
Settings,
ReloadConfig,
@@ -589,6 +598,7 @@ fn action_for_key(
(&kb.focus_pane_down, NavigateAction::FocusPaneDown),
(&kb.focus_pane_up, NavigateAction::FocusPaneUp),
(&kb.focus_pane_right, NavigateAction::FocusPaneRight),
+ (&kb.last_pane, NavigateAction::LastPane),
(&kb.cycle_pane_next, NavigateAction::CyclePaneNext),
(&kb.cycle_pane_previous, NavigateAction::CyclePanePrevious),
(&kb.split_vertical, NavigateAction::SplitVertical),
@@ -794,6 +804,10 @@ pub(super) fn execute_navigate_action_in_context(
state.cycle_pane(true);
leave_navigate_mode(state);
}
+ NavigateAction::LastPane => {
+ state.last_pane();
+ leave_navigate_mode(state);
+ }
NavigateAction::Help => super::modal::open_keybind_help(state),
NavigateAction::Settings => super::settings::open_settings(state),
NavigateAction::ReloadConfig => {
@@ -1454,6 +1468,40 @@ navigate_pane_right = "ctrl+l"
}
#[test]
+ fn terminal_direct_last_pane_shortcut_maps_to_navigation_action() {
+ let mut state = state_with_workspaces(&["test"]);
+ state.keybinds.last_pane = crate::config::ActionKeybinds::direct("alt+l");
+
+ let action = terminal_direct_navigation_action(
+ &state,
+ TerminalKey::new(KeyCode::Char('l'), KeyModifiers::ALT),
+ );
+
+ assert_eq!(action, Some(NavigateAction::LastPane));
+ }
+
+ #[test]
+ fn prefix_tab_override_can_map_to_last_pane() {
+ let config: Config = toml::from_str(
+ r#"
+[keys]
+last_pane = "prefix+tab"
+"#,
+ )
+ .unwrap();
+ let mut state = state_with_workspaces(&["test"]);
+ state.keybinds = config.keybinds();
+
+ let pane_action = action_for_key(
+ &state,
+ TerminalKey::new(KeyCode::Tab, KeyModifiers::empty()),
+ BindingDispatch::Prefix,
+ );
+
+ assert_eq!(pane_action, Some(NavigateAction::LastPane));
+ }
+
+ #[test]
fn terminal_direct_indexed_tab_shortcut_maps_to_navigation_action() {
let mut state = state_with_workspaces(&["test"]);
let config: Config = toml::from_str("[keys]\nswitch_tab = \"ctrl+3\"\n").unwrap();
@@ -1747,6 +1795,7 @@ navigate_pane_right = "ctrl+l"
app.render_dirty.clone(),
)
.expect("workspace should spawn");
+ let root_pane = workspace.tabs[0].root_pane;
app.state.workspaces = vec![workspace];
app.terminal_runtimes.insert(terminal.id.clone(), runtime);
app.state.terminals.insert(terminal.id.clone(), terminal);
@@ -1774,6 +1823,19 @@ navigate_pane_right = "ctrl+l"
assert_eq!(app.state.workspaces[0].tabs[0].layout.pane_count(), 2);
assert_eq!(app.terminal_runtimes.len(), 2);
assert!(app.state.workspaces[0].tabs[0].zoomed);
+ let overlay_pane = app.state.workspaces[0].focused_pane_id().unwrap();
+ assert_ne!(overlay_pane, root_pane);
+
+ app.state.last_pane();
+
+ assert_eq!(app.state.workspaces[0].focused_pane_id(), Some(root_pane));
+
+ app.state.last_pane();
+
+ assert_eq!(
+ app.state.workspaces[0].focused_pane_id(),
+ Some(overlay_pane)
+ );
let _ = wait_for_file(&output_path);
let deadline = std::time::Instant::now() + Duration::from_secs(2);