feat(tiles): Move Tile Left/Right/Up/Down (Ctrl+Shift+Arrows)

Four rebindable registry chords in the Tiles group move the focused
tile: it trades places with the neighbour the Alt+Shift+Arrow focus
chords pick (tileInDirection), through the same _reorderTiles path as
the header drag, and keeps the focus. Nothing at an edge.

They go through tileShortcutFor()/runTileShortcut() like the other
tile chords, so every xterm key handler swallows them while they
apply: only while the grid is open, a zoomed grid included (a no-op
there, so the keys never reach the CLI), and never in a text field
other than xterm's own textarea, where Ctrl+Shift+Arrows select by
word.

Ctrl+Shift+Arrows because every other two-modifier arrow chord is
taken: Ctrl+Alt switches workspaces (GNOME, Xfce, some Windows
graphics drivers), Ctrl+Alt+Shift moves a window to another workspace
(GNOME, Cinnamon, Xfce), Super belongs to the desktop, Alt is the
browser's back and forward, Alt+Shift focuses tiles. No browser,
GNOME, KDE, macOS or Claude Code default uses Ctrl+Shift+Arrows; it
costs a terminal editor's word selection inside a tile while the grid
is open.

The Help modal lists the chords and the header drag; the shortcut
overlay lists the registry. zh-CN entries for every new string.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-07 23:49:11 +02:00
parent 2b20288ca0
commit 20c2561a45
7 changed files with 328 additions and 6 deletions
+21 -2
View File
@@ -3,8 +3,8 @@
* that a chord which applies never reaches a PTY.
*
* Defaults (all rebindable in App Settings, Shortcuts): Ctrl+Shift+G toggles
* the grid, Alt+Shift+Arrows move focus between tiles, Remove Focused Tile is
* unbound. The toggle applies wherever a grid could open; the focus and remove
* the grid, Alt+Shift+Arrows move focus between tiles, Ctrl+Shift+Arrows move
* the focused tile (tile-grid-move.test.ts), Remove Focused Tile is unbound. The toggle applies wherever a grid could open; the focus and remove
* chords only while the grid is open, so outside it Alt+Shift+Arrows reach the
* terminal untouched. The capture-phase handler (app.js) dispatches a chord
* that applies; its preventDefault() does not stop xterm, so every xterm key
@@ -69,6 +69,11 @@ describe('registry', () => {
expect(byId[`focus-tile-${dir.toLowerCase()}`].bindings).toEqual([
{ modifiers: ['alt', 'shift'], key: `Arrow${dir}` },
]);
// Moving the focused tile (tile-grid-move.test.ts).
expect(byId[`move-tile-${dir.toLowerCase()}`].bindings).toEqual([
{ modifiers: ['ctrl', 'shift'], key: `Arrow${dir}` },
]);
expect(byId[`move-tile-${dir.toLowerCase()}`].group).toBe('Tiles');
}
expect(byId['remove-tile'].bindings).toEqual([]);
expect(byId['toggle-tile-grid'].group).toBe('Tiles');
@@ -351,6 +356,20 @@ describe('xterm key handlers swallow a chord that applies', () => {
expect(handler(chord(RIGHT))).toBe(true);
});
const MOVE_RIGHT = { key: 'ArrowRight', code: 'ArrowRight', ctrlKey: true, shiftKey: true };
it.each(['keydown', 'keypress', 'keyup'])(
'returns false for Ctrl+Shift+Right, Move Tile Right (%s), while the grid is open',
async (type) => {
const handler = await tileHandler(true);
expect(handler({ ...chord(MOVE_RIGHT), type })).toBe(false);
}
);
it('lets Ctrl+Shift+Right through to the PTY when no grid is open', async () => {
const handler = await tileHandler(false);
expect(handler(chord(MOVE_RIGHT))).toBe(true);
});
it('Alt+Shift+Enter (zoom) never becomes a Shift+Enter newline in the tile session', async () => {
const handler = await tileHandler(true);
fetchMock.mockClear();