feat(tui): add C-x o / C-2..C-5 / Shift+Arrow split-pane focus keys#618
Merged
Conversation
Fast keyboard focus movement in the split-session layout: - C-x o already cycled list → split windows → list (unchanged). - C-2..C-5 focus a pane directly (pane 1 = list, pane 2 = the first split window, …); a no-op when that pane doesn't exist. - Shift+Up/Down/Left/Right move focus to the spatially adjacent split window (emacs windmove), scoped to a focused, unzoomed multi-pane split; a no-neighbor press is a consumed no-op. Outside that context the keys keep their prior meaning (Shift+Up/Down list reorder). These are intercepted in on_key ahead of PTY forwarding so they escape a focused child's PTY the same way the C-x prefix does — otherwise they'd be useless in their main scenario (jumping out of a busy pane). Spatial adjacency uses last-frame pane geometry already tracked for mouse hit-testing. Adds spec 0061 and help-text entries; covered by unit tests for focus-by-index, list pane, and 2x2-grid spatial movement.
This was referenced Jun 29, 2026
Merged
edwin-zvs
added a commit
that referenced
this pull request
Jun 29, 2026
PR #618 added Shift+Arrow split-pane focus (emacs windmove). Shift+Left and Shift+Right work, but Shift+Up/Shift+Down appear to do nothing. Root cause is the terminal, not construct's dispatch: iTerm2, macOS Terminal.app, and GNOME Terminal all reserve Shift+Up/Shift+Down for their own line-scrollback and never forward them to the application, while Shift+Left/Shift+Right are forwarded. So the vertical axis of the feature silently dies on exactly the terminals the team uses — the same reason PR #618's own Shift+Up/Down list-reorder binding is unreliable there. The on_key dispatch and adjacency math are correct (now covered by full-path tests that drive on_key end to end, including real rendered geometry for a vertical split). Fix: add C-x <arrow> as a directional-focus alias in both keymap profiles. The C-x prefix is the one input every terminal forwards — it's how C-x o already escapes a focused child PTY — so directional pane focus now works everywhere, with Shift+Arrow kept as the fast path for cooperative terminals. C-x <arrow> reports "no split window <dir>" when there's no neighbor, since an explicit chord is a deliberate request. Updates spec 0061: directional focus must be reachable through the escape prefix, not only the bare modified-arrow keys. Adds keymap and full on_key dispatch tests for both the new chord and the existing Shift+Arrow path.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds Emacs-style fast focus-movement keys for the TUI split-session layout, so you can hop between panes without cycling.
C-x oother-window) — already existed, unchanged.C-2…C-5C-2focuses the first split window,C-3the second, … No-op when that pane doesn't exist.Shift+↑/↓/←/→windmove). Scoped to a focused, unzoomed multi-pane split; a press with no neighbor that way is a consumed no-op.How
app.rs:focus_pane_by_index(shared[list, …windows]ordering, same orderC-x owalks) andadjacent_window_id/focus_adjacent_window(closest pane whose perpendicular span overlaps the active one). Spatial adjacency reuses the per-framemain_window_areasgeometry already tracked for mouse hit-testing.on_keyahead of PTY forwarding and chord dispatch, so these keys escape a focused child's PTY exactly the way theC-xprefix does. Without that they'd be useless in their main scenario — leaving a pane that's running an interactive child. This mirrors the existing PageUp/PageDown scrollback intercept.Shift+Arrow: only takes over the arrows when the view pane is focused in an actual multi-pane split (not zoomed). On the list,Shift+↑/↓keep doing list-reorder; single-pane views pass the key through to the child. No keymap bindings were repurposed.Why an
on_keyintercept instead of new keymap entriesSwitchFocus/C-x oreaches the TUI from inside a live PTY becauseC-xis the escape prefix.C-2..C-5andShift+Arroware notC-xchords, so a keymap binding alone would be swallowed by the focused child. Handling them inon_key(the same place PageUp/PageDown and theC-x C-xliteral-escape live) is the established pattern for "global keys that shadow the child".Tradeoffs
A full-screen child in a focused split no longer receives those exact keys while a neighbor exists — the same accepted tradeoff already made for scrollback paging. Documented in spec
0061-split-pane-focus-keys-are-global.md(complements0055, whose non-goal previously said the global-chord set wasn't changing).Tests
cargo build+cargo clippyclean. New unit tests:c_digit_focuses_pane_by_index—C-2/3/4focus windows 1/2/3;C-5no-op with 3 windows.focus_pane_index_zero_is_the_list— pane 1 is the list.shift_arrow_focuses_spatially_adjacent_window— 2×2 grid: right/down/left/up neighbor resolution + no-neighbor no-op.Existing
switch_focus_cycles_*,keymap::*, and help-modal tests still pass.Where the code lives
Only
crates/cli→ the relevant binary isconstruct(target/debug/construct).