aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-15 20:31:01 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-15 20:31:01 +0300
commit4cbe72ec2d8a174e3c4f9bde2101aa189c7f38e7 (patch)
tree2de696538e494d3c14a920c20877bcd6d4ac39dc /src/app
parent43d60fc6629d222ae63813ebcf4531067ba5bc9d (diff)
feat: add indexed keybind families
Diffstat (limited to 'src/app')
-rw-r--r--src/app/actions.rs70
-rw-r--r--src/app/input/navigate.rs75
-rw-r--r--src/app/state.rs6
3 files changed, 133 insertions, 18 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index 9477ca15..21e17bf0 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -331,6 +331,32 @@ impl AppState {
self.cycle_agent_entry(false);
}
+ pub fn focus_agent_entry(&mut self, idx: usize) -> bool {
+ let entries = crate::ui::agent_panel_entries(self);
+ let Some(target) = entries.get(idx) else {
+ return false;
+ };
+ let ws_idx = target.ws_idx;
+ let tab_idx = target.tab_idx;
+ let pane_id = target.pane_id;
+
+ self.switch_workspace(ws_idx);
+ self.switch_tab(tab_idx);
+ if let Some(tab) = self
+ .workspaces
+ .get_mut(ws_idx)
+ .and_then(|ws| ws.tabs.get_mut(tab_idx))
+ {
+ if tab.panes.contains_key(&pane_id) {
+ tab.layout.focus_pane(pane_id);
+ self.mark_session_dirty();
+ self.ensure_agent_panel_entry_visible(idx);
+ return true;
+ }
+ }
+ false
+ }
+
fn cycle_agent_entry(&mut self, forward: bool) {
let entries = crate::ui::agent_panel_entries(self);
if entries.is_empty() {
@@ -351,24 +377,7 @@ impl AppState {
(None, false) => entries.len() - 1,
};
- let target = &entries[target_idx];
- let ws_idx = target.ws_idx;
- let tab_idx = target.tab_idx;
- let pane_id = target.pane_id;
-
- self.switch_workspace(ws_idx);
- self.switch_tab(tab_idx);
- if let Some(tab) = self
- .workspaces
- .get_mut(ws_idx)
- .and_then(|ws| ws.tabs.get_mut(tab_idx))
- {
- if tab.panes.contains_key(&pane_id) {
- tab.layout.focus_pane(pane_id);
- self.mark_session_dirty();
- }
- }
- self.ensure_agent_panel_entry_visible(target_idx);
+ self.focus_agent_entry(target_idx);
}
fn ensure_agent_panel_entry_visible(&mut self, idx: usize) {
@@ -987,6 +996,31 @@ mod tests {
}
#[test]
+ fn focus_agent_entry_uses_agent_panel_order() {
+ let mut first = Workspace::test_new("one");
+ let first_root = first.tabs[0].root_pane;
+ let first_second = first.test_split(Direction::Horizontal);
+ first.tabs[0].layout.focus_pane(first_root);
+ let second = Workspace::test_new("two");
+ let second_root = second.tabs[0].root_pane;
+
+ let mut state = AppState::test_new();
+ state.workspaces = vec![first, second];
+ state.active = Some(0);
+ state.selected = 0;
+ state.mode = Mode::Terminal;
+ state.agent_panel_scope = crate::app::state::AgentPanelScope::AllWorkspaces;
+ mark_agent(&mut state, 0, 0, first_root);
+ mark_agent(&mut state, 0, 0, first_second);
+ mark_agent(&mut state, 1, 0, second_root);
+
+ assert!(state.focus_agent_entry(2));
+
+ assert_eq!(state.active, Some(1));
+ assert_eq!(state.workspaces[1].focused_pane_id(), Some(second_root));
+ }
+
+ #[test]
fn next_agent_cycles_only_current_scope_entries() {
let mut first = Workspace::test_new("one");
let first_root = first.tabs[0].root_pane;
diff --git a/src/app/input/navigate.rs b/src/app/input/navigate.rs
index 6dfaf475..7675835b 100644
--- a/src/app/input/navigate.rs
+++ b/src/app/input/navigate.rs
@@ -17,6 +17,10 @@ pub(crate) fn terminal_direct_navigation_action(
state: &AppState,
key: &KeyEvent,
) -> Option<NavigateAction> {
+ if let Some(action) = indexed_navigation_action(state, key) {
+ return Some(action);
+ }
+
let kb = &state.keybinds;
if kb
.previous_workspace
@@ -373,6 +377,9 @@ pub(crate) enum NavigateAction {
NewWorkspace,
RenameWorkspace,
CloseWorkspace,
+ SwitchWorkspace(usize),
+ SwitchTab(usize),
+ FocusAgent(usize),
PreviousWorkspace,
NextWorkspace,
PreviousAgent,
@@ -398,7 +405,40 @@ pub(crate) enum NavigateAction {
Detach,
}
+fn indexed_navigation_action(state: &AppState, key: &KeyEvent) -> Option<NavigateAction> {
+ let KeyCode::Char(c @ '1'..='9') = key.code else {
+ return None;
+ };
+ let idx = (c as usize) - ('1' as usize);
+ let kb = &state.keybinds;
+
+ if kb
+ .indexed_tabs
+ .is_some_and(|mods| key_matches(key, KeyCode::Char(c), mods))
+ {
+ return Some(NavigateAction::SwitchTab(idx));
+ }
+ if kb
+ .indexed_workspaces
+ .is_some_and(|mods| key_matches(key, KeyCode::Char(c), mods))
+ {
+ return Some(NavigateAction::SwitchWorkspace(idx));
+ }
+ if kb
+ .indexed_agents
+ .is_some_and(|mods| key_matches(key, KeyCode::Char(c), mods))
+ {
+ return Some(NavigateAction::FocusAgent(idx));
+ }
+
+ None
+}
+
fn navigate_action_for_key(state: &AppState, key: &KeyEvent) -> Option<NavigateAction> {
+ if let Some(action) = indexed_navigation_action(state, key) {
+ return Some(action);
+ }
+
let kb = &state.keybinds;
if key_matches(key, kb.new_workspace.0, kb.new_workspace.1) {
return Some(NavigateAction::NewWorkspace);
@@ -526,6 +566,27 @@ pub(super) fn execute_navigate_action(state: &mut AppState, action: NavigateActi
}
}
}
+ NavigateAction::SwitchWorkspace(idx) => {
+ if idx < state.workspaces.len() {
+ state.switch_workspace(idx);
+ leave_navigate_mode(state);
+ }
+ }
+ NavigateAction::SwitchTab(idx) => {
+ let tab_exists = state
+ .active
+ .and_then(|ws_idx| state.workspaces.get(ws_idx))
+ .is_some_and(|ws| idx < ws.tabs.len());
+ if tab_exists {
+ state.switch_tab(idx);
+ leave_navigate_mode(state);
+ }
+ }
+ NavigateAction::FocusAgent(idx) => {
+ if state.focus_agent_entry(idx) {
+ leave_navigate_mode(state);
+ }
+ }
NavigateAction::PreviousWorkspace => {
state.previous_workspace();
leave_navigate_mode(state);
@@ -791,6 +852,20 @@ mod tests {
assert_eq!(action, Some(NavigateAction::FocusPaneLeft));
}
+ #[test]
+ fn terminal_direct_indexed_tab_shortcut_maps_to_navigation_action() {
+ let mut state = state_with_workspaces(&["test"]);
+ state.keybinds.indexed_tabs = Some(KeyModifiers::CONTROL);
+ state.keybinds.indexed_tabs_label = Some("ctrl+1..9".into());
+
+ let action = terminal_direct_navigation_action(
+ &state,
+ &KeyEvent::new(KeyCode::Char('3'), KeyModifiers::CONTROL),
+ );
+
+ assert_eq!(action, Some(NavigateAction::SwitchTab(2)));
+ }
+
#[tokio::test]
async fn custom_command_runs_from_prefix_key_in_navigate_mode() {
let (_api_tx, api_rx) = tokio::sync::mpsc::unbounded_channel();
diff --git a/src/app/state.rs b/src/app/state.rs
index 383f2bd5..d0d40c72 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -1095,6 +1095,12 @@ impl AppState {
previous_agent_label: None,
next_agent: None,
next_agent_label: None,
+ indexed_tabs: None,
+ indexed_tabs_label: None,
+ indexed_workspaces: None,
+ indexed_workspaces_label: None,
+ indexed_agents: None,
+ indexed_agents_label: None,
new_tab: (KeyCode::Char('c'), KeyModifiers::empty()),
new_tab_label: "c".into(),
rename_tab: None,