diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-02 05:10:13 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-02 23:24:46 +0300 |
| commit | b695adee60e50764f4713c8e586ca02fb844f99a (patch) | |
| tree | 6fd769682289fc8053e3cf966ad2ca2d5d4898dd /src/app/input.rs | |
| parent | 28fb23cdaf4fce03ed6dbfcb4bb13be324b181ac (diff) | |
feat: add tabs within workspaces
Diffstat (limited to 'src/app/input.rs')
| -rw-r--r-- | src/app/input.rs | 258 |
1 files changed, 252 insertions, 6 deletions
diff --git a/src/app/input.rs b/src/app/input.rs index e0f012e9..9a4c9aaf 100644 --- a/src/app/input.rs +++ b/src/app/input.rs @@ -32,6 +32,35 @@ use super::App; // Key handling // --------------------------------------------------------------------------- +fn terminal_direct_navigation_action(state: &AppState, key: &KeyEvent) -> Option<NavigateAction> { + let kb = &state.keybinds; + if kb + .previous_workspace + .is_some_and(|(code, mods)| key_matches(key, code, mods)) + { + return Some(NavigateAction::PreviousWorkspace); + } + if kb + .next_workspace + .is_some_and(|(code, mods)| key_matches(key, code, mods)) + { + return Some(NavigateAction::NextWorkspace); + } + if kb + .previous_tab + .is_some_and(|(code, mods)| key_matches(key, code, mods)) + { + return Some(NavigateAction::PreviousTab); + } + if kb + .next_tab + .is_some_and(|(code, mods)| key_matches(key, code, mods)) + { + return Some(NavigateAction::NextTab); + } + None +} + impl App { pub(super) async fn handle_key(&mut self, key: TerminalKey) { match self.state.mode { @@ -42,7 +71,9 @@ impl App { Mode::Onboarding => self.handle_onboarding_key(key), Mode::ReleaseNotes => self.handle_release_notes_key(key), Mode::Navigate => handle_navigate_key(&mut self.state, key), - Mode::RenameSession => handle_rename_key(&mut self.state, key), + Mode::RenameWorkspace | Mode::RenameTab => { + handle_rename_key(&mut self.state, key) + } Mode::Resize => handle_resize_key(&mut self.state, key), Mode::ConfirmClose => handle_confirm_close_key(&mut self.state, key), Mode::ContextMenu => handle_context_menu_key(&mut self.state, key), @@ -223,6 +254,11 @@ impl App { let key_event = key.as_key_event(); + if let Some(action) = terminal_direct_navigation_action(&self.state, &key_event) { + execute_navigate_action(&mut self.state, action); + return; + } + if self.state.is_prefix(&key_event) { self.state.mode = Mode::Navigate; return; @@ -446,6 +482,13 @@ enum NavigateAction { NewWorkspace, RenameWorkspace, CloseWorkspace, + PreviousWorkspace, + NextWorkspace, + NewTab, + RenameTab, + PreviousTab, + NextTab, + CloseTab, SplitVertical, SplitHorizontal, ClosePane, @@ -465,6 +508,45 @@ fn navigate_action_for_key(state: &AppState, key: &KeyEvent) -> Option<NavigateA if key_matches(key, kb.close_workspace.0, kb.close_workspace.1) { return Some(NavigateAction::CloseWorkspace); } + if kb + .previous_workspace + .is_some_and(|(code, mods)| key_matches(key, code, mods)) + { + return Some(NavigateAction::PreviousWorkspace); + } + if kb + .next_workspace + .is_some_and(|(code, mods)| key_matches(key, code, mods)) + { + return Some(NavigateAction::NextWorkspace); + } + if key_matches(key, kb.new_tab.0, kb.new_tab.1) { + return Some(NavigateAction::NewTab); + } + if kb + .rename_tab + .is_some_and(|(code, mods)| key_matches(key, code, mods)) + { + return Some(NavigateAction::RenameTab); + } + if kb + .previous_tab + .is_some_and(|(code, mods)| key_matches(key, code, mods)) + { + return Some(NavigateAction::PreviousTab); + } + if kb + .next_tab + .is_some_and(|(code, mods)| key_matches(key, code, mods)) + { + return Some(NavigateAction::NextTab); + } + if kb + .close_tab + .is_some_and(|(code, mods)| key_matches(key, code, mods)) + { + return Some(NavigateAction::CloseTab); + } if key_matches(key, kb.split_vertical.0, kb.split_vertical.1) { return Some(NavigateAction::SplitVertical); } @@ -495,7 +577,7 @@ fn execute_navigate_action(state: &mut AppState, action: NavigateAction) { NavigateAction::RenameWorkspace => { if !state.workspaces.is_empty() { state.name_input = state.workspaces[state.selected].display_name(); - state.mode = Mode::RenameSession; + state.mode = Mode::RenameWorkspace; } } NavigateAction::CloseWorkspace => { @@ -508,6 +590,38 @@ fn execute_navigate_action(state: &mut AppState, action: NavigateAction) { } } } + NavigateAction::PreviousWorkspace => { + state.previous_workspace(); + leave_navigate_mode(state); + } + NavigateAction::NextWorkspace => { + state.next_workspace(); + leave_navigate_mode(state); + } + NavigateAction::NewTab => { + state.request_new_tab = true; + leave_navigate_mode(state); + } + NavigateAction::RenameTab => { + if let Some(ws) = state.active.and_then(|i| state.workspaces.get(i)) { + if let Some(name) = ws.active_tab_display_name() { + state.name_input = name; + state.mode = Mode::RenameTab; + } + } + } + NavigateAction::PreviousTab => { + state.previous_tab(); + leave_navigate_mode(state); + } + NavigateAction::NextTab => { + state.next_tab(); + leave_navigate_mode(state); + } + NavigateAction::CloseTab => { + state.close_tab(); + leave_navigate_mode(state); + } NavigateAction::SplitVertical => { state.split_pane(Direction::Horizontal); leave_navigate_mode(state); @@ -567,8 +681,20 @@ fn handle_rename_key(state: &mut AppState, key: KeyEvent) { } else { state.name_input.trim().to_string() }; - if !new_name.is_empty() && !state.workspaces.is_empty() { - state.workspaces[state.selected].set_custom_name(new_name); + if !new_name.is_empty() { + match state.mode { + Mode::RenameWorkspace if !state.workspaces.is_empty() => { + state.workspaces[state.selected].set_custom_name(new_name); + } + Mode::RenameTab => { + if let Some(ws) = state.active.and_then(|i| state.workspaces.get_mut(i)) { + if let Some(tab) = ws.active_tab_mut() { + tab.set_custom_name(new_name); + } + } + } + _ => {} + } } state.name_input.clear(); state.mode = Mode::Navigate; @@ -577,6 +703,9 @@ fn handle_rename_key(state: &mut AppState, key: KeyEvent) { state.name_input.clear(); state.mode = Mode::Navigate; } + KeyCode::Char('c') if key.modifiers == crossterm::event::KeyModifiers::CONTROL => { + state.name_input.clear(); + } KeyCode::Backspace => { state.name_input.pop(); } @@ -653,7 +782,7 @@ fn apply_context_menu_action(state: &mut AppState, menu: ContextMenuState, idx: (ContextMenuKind::Workspace { ws_idx }, Some("Rename")) => { state.selected = ws_idx; state.name_input = state.workspaces[ws_idx].display_name(); - state.mode = Mode::RenameSession; + state.mode = Mode::RenameWorkspace; } (ContextMenuKind::Workspace { ws_idx }, Some("Close")) => { state.selected = ws_idx; @@ -664,6 +793,35 @@ fn apply_context_menu_action(state: &mut AppState, menu: ContextMenuState, idx: state.mode = Mode::Navigate; } } + (ContextMenuKind::Tab { ws_idx, tab_idx }, Some("New tab")) => { + state.selected = ws_idx; + state.active = Some(ws_idx); + state.switch_tab(tab_idx); + state.request_new_tab = true; + state.mode = Mode::Terminal; + } + (ContextMenuKind::Tab { ws_idx, tab_idx }, Some("Rename")) => { + state.selected = ws_idx; + state.active = Some(ws_idx); + state.switch_tab(tab_idx); + if let Some(ws) = state.workspaces.get(ws_idx) { + if let Some(name) = ws.active_tab_display_name() { + state.name_input = name; + state.mode = Mode::RenameTab; + } + } + } + (ContextMenuKind::Tab { ws_idx, tab_idx }, Some("Close")) => { + state.selected = ws_idx; + state.active = Some(ws_idx); + state.switch_tab(tab_idx); + state.close_tab(); + state.mode = if state.active.is_some() { + Mode::Terminal + } else { + Mode::Navigate + }; + } (ContextMenuKind::Pane, Some("Split vertical")) => { state.split_pane(Direction::Horizontal); state.mode = Mode::Terminal; @@ -760,6 +918,30 @@ impl AppState { && row < button.y + button.height } + fn rename_modal_inner(&self) -> Option<Rect> { + self.onboarding_modal_inner(56, 7) + } + + fn rename_button_at(&self, col: u16, row: u16) -> Option<&'static str> { + let inner = self.rename_modal_inner()?; + if inner.height < 4 || inner.width < 28 { + return None; + } + let (save, clear, cancel) = crate::ui::rename_button_rects(inner); + if row != save.y { + return None; + } + if col >= save.x && col < save.x + save.width { + Some("save") + } else if col >= clear.x && col < clear.x + clear.width { + Some("clear") + } else if col >= cancel.x && col < cancel.x + cancel.width { + Some("cancel") + } else { + None + } + } + fn release_notes_body_rect(&self) -> Option<Rect> { let inner = self.release_notes_modal_inner()?; if inner.height < 8 || inner.width < 4 { @@ -1082,6 +1264,19 @@ impl AppState { return None; } + if matches!(self.mode, Mode::RenameWorkspace | Mode::RenameTab) { + match self.rename_button_at(mouse.column, mouse.row) { + Some("save") => handle_rename_key(self, KeyEvent::from(KeyCode::Enter)), + Some("clear") => self.name_input.clear(), + Some("cancel") => handle_rename_key(self, KeyEvent::from(KeyCode::Esc)), + None => { + handle_rename_key(self, KeyEvent::from(KeyCode::Esc)); + } + _ => {} + } + return None; + } + if self.mode == Mode::ContextMenu { let item_idx = self.context_menu_item_at(mouse.column, mouse.row); if let Some(menu) = self.context_menu.take() { @@ -1139,6 +1334,17 @@ impl AppState { } } + if let Some(tab_idx) = self.tab_at(mouse.column, mouse.row) { + self.switch_tab(tab_idx); + self.mode = Mode::Terminal; + return None; + } + if self.on_new_tab_button(mouse.column, mouse.row) { + self.request_new_tab = true; + self.mode = Mode::Terminal; + return None; + } + if in_sidebar { if self.sidebar_collapsed { let idx = (mouse.row - sidebar.y) as usize; @@ -1300,6 +1506,23 @@ impl AppState { } } + MouseEventKind::Down(MouseButton::Right) + if self.tab_at(mouse.column, mouse.row).is_some() => + { + if let (Some(ws_idx), Some(tab_idx)) = + (self.active, self.tab_at(mouse.column, mouse.row)) + { + self.switch_tab(tab_idx); + self.context_menu = Some(ContextMenuState { + kind: ContextMenuKind::Tab { ws_idx, tab_idx }, + x: mouse.column, + y: mouse.row, + selected: 0, + }); + self.mode = Mode::ContextMenu; + } + } + MouseEventKind::Down(MouseButton::Right) if !in_sidebar => { if self.pane_at(mouse.column, mouse.row).is_some() { self.context_menu = Some(ContextMenuState { @@ -1337,6 +1560,29 @@ impl AppState { } /// Find which workspace index a sidebar row belongs to (two-section layout). + fn tab_at(&self, col: u16, row: u16) -> Option<usize> { + self.view + .tab_hit_areas + .iter() + .enumerate() + .find_map(|(idx, area)| { + (row >= area.y + && row < area.y + area.height + && col >= area.x + && col < area.x + area.width) + .then_some(idx) + }) + } + + fn on_new_tab_button(&self, col: u16, row: u16) -> bool { + let area = self.view.new_tab_hit_area; + area.width > 0 + && row >= area.y + && row < area.y + area.height + && col >= area.x + && col < area.x + area.width + } + fn workspace_at_row(&self, row: u16) -> Option<usize> { let sidebar = self.view.sidebar_rect; let total_h = sidebar.height as usize; @@ -1744,7 +1990,7 @@ mod tests { KeyEvent::new(KeyCode::Char('g'), KeyModifiers::empty()), ); - assert_eq!(state.mode, Mode::RenameSession); + assert_eq!(state.mode, Mode::RenameWorkspace); assert_eq!(state.name_input, "test"); } |
