diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-15 20:31:01 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-15 20:31:01 +0300 |
| commit | 4cbe72ec2d8a174e3c4f9bde2101aa189c7f38e7 (patch) | |
| tree | 2de696538e494d3c14a920c20877bcd6d4ac39dc /src/app | |
| parent | 43d60fc6629d222ae63813ebcf4531067ba5bc9d (diff) | |
feat: add indexed keybind families
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/actions.rs | 70 | ||||
| -rw-r--r-- | src/app/input/navigate.rs | 75 | ||||
| -rw-r--r-- | src/app/state.rs | 6 |
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, |
