Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ impl App {
Mode::OpenExistingWorktree => self.handle_worktree_open_key(key_event),
Mode::ConfirmRemoveWorktree => self.handle_worktree_remove_key(key_event),
Mode::Resize => self.handle_resize_key_via_api(key),
Mode::FocusNav => self.handle_focus_nav_key(key),
Mode::ConfirmClose => self.handle_confirm_close_key_via_api(key_event),
Mode::ContextMenu => {
self.handle_context_menu_key_via_api(key_event);
Expand Down
59 changes: 59 additions & 0 deletions src/app/input/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,65 @@ impl App {
}
}

/// Persistent focus-navigation mode (like resize mode, but moves focus).
/// Stays open so you can chain moves; esc/enter or re-pressing the trigger exits.
/// h/j/k/l or arrows -> focus pane in that direction
/// Tab / Shift+Tab -> next / previous tab
/// ] / [ -> next / previous workspace
pub(crate) fn handle_focus_nav_key(&mut self, raw_key: TerminalKey) {
let key = raw_key.as_key_event();
self.state.update_dismissed = true;

if key.code == KeyCode::Esc
|| key.code == KeyCode::Enter
|| self.state.keybinds.focus_nav.matches_prefix_key(&raw_key)
|| self.state.keybinds.focus_nav.matches_direct_key(&raw_key)
{
self.state.mode = if self.state.active.is_some() {
Mode::Terminal
} else {
Mode::Navigate
};
return;
}

match key.code {
KeyCode::Char('h') | KeyCode::Left => {
self.focus_pane_direction_via_api(NavDirection::Left)
}
KeyCode::Char('j') | KeyCode::Down => {
self.focus_pane_direction_via_api(NavDirection::Down)
}
KeyCode::Char('k') | KeyCode::Up => {
self.focus_pane_direction_via_api(NavDirection::Up)
}
KeyCode::Char('l') | KeyCode::Right => {
self.focus_pane_direction_via_api(NavDirection::Right)
}
KeyCode::Tab => {
if let Some(tab_idx) = self.relative_tab(1) {
self.focus_tab_idx_via_api(tab_idx);
}
}
KeyCode::BackTab => {
if let Some(tab_idx) = self.relative_tab(-1) {
self.focus_tab_idx_via_api(tab_idx);
}
}
KeyCode::Char(']') => {
if let Some(ws_idx) = self.relative_visible_workspace(1) {
self.focus_workspace_idx_via_api(ws_idx);
}
}
KeyCode::Char('[') => {
if let Some(ws_idx) = self.relative_visible_workspace(-1) {
self.focus_workspace_idx_via_api(ws_idx);
}
}
_ => {}
}
}

pub(crate) fn handle_confirm_close_key_via_api(&mut self, key: KeyEvent) {
match modal_action_from_key(&key, CONFIRM_CLOSE_ACTIONS) {
Some(ModalAction::Confirm) => {
Expand Down
8 changes: 6 additions & 2 deletions src/app/input/navigate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ impl App {
leave_navigate_mode(&mut self.state);
}
NavigateAction::EnterResizeMode => self.state.mode = Mode::Resize,
NavigateAction::EnterFocusNav => self.state.mode = Mode::FocusNav,
NavigateAction::ResizePaneLeft => {
self.resize_pane_direction_via_api(NavDirection::Left);
leave_navigate_mode(&mut self.state);
Expand Down Expand Up @@ -750,7 +751,7 @@ impl App {
Some((ws_idx, focused.id, target))
}

fn relative_visible_workspace(&self, delta: isize) -> Option<usize> {
pub(super) fn relative_visible_workspace(&self, delta: isize) -> Option<usize> {
let order = self.state.visible_workspace_order();
if order.is_empty() {
return None;
Expand All @@ -769,7 +770,7 @@ impl App {
Some((ws_idx, source, insert))
}

fn relative_tab(&self, delta: isize) -> Option<usize> {
pub(super) fn relative_tab(&self, delta: isize) -> Option<usize> {
let ws = self
.state
.active
Expand Down Expand Up @@ -1416,6 +1417,7 @@ pub(crate) enum NavigateAction {
CopyMode,
Zoom,
EnterResizeMode,
EnterFocusNav,
ResizePaneLeft,
ResizePaneDown,
ResizePaneUp,
Expand Down Expand Up @@ -1566,6 +1568,7 @@ fn non_indexed_action_for_key(
(&kb.close_pane, NavigateAction::ClosePane),
(&kb.zoom, NavigateAction::Zoom),
(&kb.resize_mode, NavigateAction::EnterResizeMode),
(&kb.focus_nav, NavigateAction::EnterFocusNav),
(&kb.resize_pane_left, NavigateAction::ResizePaneLeft),
(&kb.resize_pane_down, NavigateAction::ResizePaneDown),
(&kb.resize_pane_up, NavigateAction::ResizePaneUp),
Expand Down Expand Up @@ -1809,6 +1812,7 @@ pub(super) fn execute_navigate_action_in_context(
leave_navigate_mode(state);
}
NavigateAction::EnterResizeMode => state.mode = Mode::Resize,
NavigateAction::EnterFocusNav => state.mode = Mode::FocusNav,
NavigateAction::ResizePaneLeft => {
state.resize_pane(NavDirection::Left);
leave_navigate_mode(state);
Expand Down
3 changes: 3 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,9 @@ impl App {
Mode::Resize => {
self.handle_resize_key_via_api(key);
}
Mode::FocusNav => {
self.handle_focus_nav_key(key);
}
Mode::ConfirmClose => {
self.handle_confirm_close_key_via_api(key_event);
}
Expand Down
2 changes: 2 additions & 0 deletions src/app/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ pub enum Mode {
OpenExistingWorktree,
ConfirmRemoveWorktree,
Resize,
FocusNav,
ConfirmClose,
ContextMenu,
Settings,
Expand Down Expand Up @@ -823,6 +824,7 @@ impl Mode {
| Mode::Navigator
| Mode::Copy
| Mode::Resize
| Mode::FocusNav
| Mode::ConfirmClose
| Mode::ConfirmRemoveWorktree
| Mode::ContextMenu
Expand Down
3 changes: 3 additions & 0 deletions src/config/keybinds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ pub struct Keybinds {
pub close_pane: ActionKeybinds,
pub zoom: ActionKeybinds,
pub resize_mode: ActionKeybinds,
pub focus_nav: ActionKeybinds,
pub resize_pane_left: ActionKeybinds,
pub resize_pane_down: ActionKeybinds,
pub resize_pane_up: ActionKeybinds,
Expand Down Expand Up @@ -520,6 +521,7 @@ impl Config {
close_pane: empty_action!(),
zoom: empty_action!(),
resize_mode: empty_action!(),
focus_nav: empty_action!(),
resize_pane_left: empty_action!(),
resize_pane_down: empty_action!(),
resize_pane_up: empty_action!(),
Expand Down Expand Up @@ -667,6 +669,7 @@ impl Config {
apply_action!(keybinds.close_pane, close_pane, source);
apply_action!(keybinds.zoom, zoom, source);
apply_action!(keybinds.resize_mode, resize_mode, source);
apply_action!(keybinds.focus_nav, focus_nav, source);
apply_action!(keybinds.resize_pane_left, resize_pane_left, source);
apply_action!(keybinds.resize_pane_down, resize_pane_down, source);
apply_action!(keybinds.resize_pane_up, resize_pane_up, source);
Expand Down
9 changes: 9 additions & 0 deletions src/config/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,10 @@ pub struct KeysConfig {
pub zoom: BindingConfig,
/// Enter resize mode. Default: "prefix+r"
pub resize_mode: BindingConfig,
/// Enter focus-navigation mode: a persistent mode where h/j/k/l move pane
/// focus, Tab/Shift+Tab switch tabs, and ] / [ switch workspaces, staying
/// open until Escape. Unset by default.
pub focus_nav: BindingConfig,
/// Resize the focused pane toward the left. Unset by default.
pub resize_pane_left: BindingConfig,
/// Resize the focused pane downward. Unset by default.
Expand Down Expand Up @@ -573,6 +577,8 @@ pub(crate) struct KeysConfigOverlay {
#[serde(skip_serializing_if = "Option::is_none")]
resize_mode: Option<BindingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
focus_nav: Option<BindingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
resize_pane_left: Option<BindingConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
resize_pane_down: Option<BindingConfig>,
Expand Down Expand Up @@ -659,6 +665,7 @@ impl<'de> Deserialize<'de> for KeysConfig {
apply_field!(close_pane);
apply_field!(zoom);
apply_field!(resize_mode);
apply_field!(focus_nav);
apply_field!(resize_pane_left);
apply_field!(resize_pane_down);
apply_field!(resize_pane_up);
Expand Down Expand Up @@ -763,6 +770,7 @@ impl KeysConfig {
copy_effective_action_field!(close_pane, keybinds.close_pane);
copy_effective_action_field!(zoom, keybinds.zoom);
copy_effective_action_field!(resize_mode, keybinds.resize_mode);
copy_effective_action_field!(focus_nav, keybinds.focus_nav);
copy_effective_action_field!(resize_pane_left, keybinds.resize_pane_left);
copy_effective_action_field!(resize_pane_down, keybinds.resize_pane_down);
copy_effective_action_field!(resize_pane_up, keybinds.resize_pane_up);
Expand Down Expand Up @@ -1064,6 +1072,7 @@ impl Default for KeysConfig {
close_pane: BindingConfig::one("prefix+x"),
zoom: BindingConfig::one("prefix+z"),
resize_mode: BindingConfig::one("prefix+r"),
focus_nav: BindingConfig::empty(),
resize_pane_left: BindingConfig::empty(),
resize_pane_down: BindingConfig::empty(),
resize_pane_up: BindingConfig::empty(),
Expand Down
6 changes: 4 additions & 2 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ use self::dialogs::{
};
use self::keybind_help::render_keybind_help_overlay;
use self::menus::{
render_context_menu, render_copy_mode_overlay, render_global_launcher_menu,
render_navigate_overlay, render_prefix_overlay, render_resize_overlay,
render_context_menu, render_copy_mode_overlay, render_focus_nav_overlay,
render_global_launcher_menu, render_navigate_overlay, render_prefix_overlay,
render_resize_overlay,
};
use self::mobile::{
compute_mobile_header_hit_areas, is_mobile_width, mobile_switcher_max_scroll_for_height,
Expand Down Expand Up @@ -439,6 +440,7 @@ pub fn render_with_runtime_registry(
Mode::Prefix => render_prefix_overlay(app, frame, mode_bar_area),
Mode::Copy => render_copy_mode_overlay(app, frame, mode_bar_area),
Mode::Resize => render_resize_overlay(app, frame, mode_bar_area),
Mode::FocusNav => render_focus_nav_overlay(app, frame, mode_bar_area),
Mode::ConfirmClose => {
render_confirm_close_overlay(app, terminal_runtimes, frame, terminal_area)
}
Expand Down
29 changes: 29 additions & 0 deletions src/ui/menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,35 @@ pub(super) fn render_resize_overlay(app: &AppState, frame: &mut Frame, area: Rec
render_bottom_bar(frame, overlay_area, line, app.palette.panel_bg);
}

pub(super) fn render_focus_nav_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
let key = Style::default()
.fg(app.palette.accent)
.add_modifier(Modifier::BOLD);
let dim = Style::default().fg(app.palette.overlay0);

let mode_style = Style::default()
.fg(panel_contrast_fg(&app.palette))
.bg(app.palette.green)
.add_modifier(Modifier::BOLD);

let line = Line::from(vec![
Span::styled(" NAV ", mode_style),
Span::raw(" "),
Span::styled("hjkl", key),
Span::styled(" pane ", dim),
Span::styled("⇥", key),
Span::styled(" tab ", dim),
Span::styled("[ ]", key),
Span::styled(" ws ", dim),
Span::styled("esc", key),
Span::styled(" done", dim),
]);

let overlay_y = area.y + area.height.saturating_sub(1);
let overlay_area = Rect::new(area.x, overlay_y, area.width, 1);
render_bottom_bar(frame, overlay_area, line, app.palette.panel_bg);
}

pub(super) fn render_context_menu(app: &AppState, frame: &mut Frame) {
let Some(menu) = &app.context_menu else {
return;
Expand Down
Loading