mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
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:
@@ -579,6 +579,45 @@ const DEFAULT_SHORTCUTS = [
|
||||
bindings: [{ modifiers: ['alt', 'shift'], key: 'ArrowDown' }],
|
||||
action: 'focusTileDown',
|
||||
},
|
||||
// Move the focused tile: it trades places with the neighbour the focus
|
||||
// chords would pick. Ctrl+Shift+Arrows because every other two-modifier
|
||||
// arrow chord is taken: Ctrl+Alt+Arrows switch workspaces (GNOME, Xfce, and
|
||||
// rotate the screen on some Windows graphics drivers), Ctrl+Alt+Shift+Arrows
|
||||
// move a window to another workspace (GNOME, Cinnamon, Xfce), Super chords
|
||||
// belong to the desktop, Alt+Arrows are the browser's back and forward, and
|
||||
// Alt+Shift+Arrows focus tiles. No browser, GNOME, KDE or macOS default and
|
||||
// no Claude Code default uses Ctrl+Shift+Arrows (it parallels Ctrl+Shift+{ }
|
||||
// for moving tabs); what it costs is word selection, so the chords skip a
|
||||
// text field (tileShortcutFor) and give up only a terminal editor's
|
||||
// word selection (nano, micro, emacs) inside a tile while the grid is open.
|
||||
{
|
||||
id: 'move-tile-left',
|
||||
group: 'Tiles',
|
||||
label: 'Move Tile Left',
|
||||
bindings: [{ modifiers: ['ctrl', 'shift'], key: 'ArrowLeft' }],
|
||||
action: 'moveTileLeft',
|
||||
},
|
||||
{
|
||||
id: 'move-tile-right',
|
||||
group: 'Tiles',
|
||||
label: 'Move Tile Right',
|
||||
bindings: [{ modifiers: ['ctrl', 'shift'], key: 'ArrowRight' }],
|
||||
action: 'moveTileRight',
|
||||
},
|
||||
{
|
||||
id: 'move-tile-up',
|
||||
group: 'Tiles',
|
||||
label: 'Move Tile Up',
|
||||
bindings: [{ modifiers: ['ctrl', 'shift'], key: 'ArrowUp' }],
|
||||
action: 'moveTileUp',
|
||||
},
|
||||
{
|
||||
id: 'move-tile-down',
|
||||
group: 'Tiles',
|
||||
label: 'Move Tile Down',
|
||||
bindings: [{ modifiers: ['ctrl', 'shift'], key: 'ArrowDown' }],
|
||||
action: 'moveTileDown',
|
||||
},
|
||||
{
|
||||
id: 'zoom-tile',
|
||||
group: 'Tiles',
|
||||
|
||||
@@ -114,6 +114,14 @@
|
||||
'Focus Tile Up': '聚焦上方窗格',
|
||||
'Focus Tile Down': '聚焦下方窗格',
|
||||
'Focus Tile Left / Right / Up / Down': '聚焦左侧 / 右侧 / 上方 / 下方窗格',
|
||||
'Move Tile Left': '向左移动窗格',
|
||||
'Move Tile Right': '向右移动窗格',
|
||||
'Move Tile Up': '向上移动窗格',
|
||||
'Move Tile Down': '向下移动窗格',
|
||||
'Move Tile Left / Right / Up / Down': '向左 / 右 / 上 / 下移动窗格',
|
||||
Drag: '拖动',
|
||||
"a tile's header": '窗格的标题栏',
|
||||
'Move the Tile (onto Another: Swap)': '移动窗格(拖到另一个窗格上:互换位置)',
|
||||
'Zoom Focused Tile': '放大聚焦的窗格',
|
||||
'Remove Focused Tile': '移除聚焦的窗格',
|
||||
'Add the Session to the Tile Grid': '将该会话加入平铺网格',
|
||||
|
||||
@@ -838,6 +838,8 @@
|
||||
<div class="shortcuts-grid">
|
||||
<div><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd></div><div>Toggle Tile Grid</div>
|
||||
<div><kbd>Alt/Option</kbd>+<kbd>Shift</kbd>+<kbd>Arrows</kbd></div><div>Focus Tile Left / Right / Up / Down</div>
|
||||
<div><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Arrows</kbd></div><div>Move Tile Left / Right / Up / Down</div>
|
||||
<div><kbd>Drag</kbd> a tile's header</div><div>Move the Tile (onto Another: Swap)</div>
|
||||
<div><kbd>Alt/Option</kbd>+<kbd>Shift</kbd>+<kbd>Enter</kbd></div><div>Zoom Focused Tile</div>
|
||||
<div><kbd>Ctrl/Cmd</kbd>+<kbd>Click</kbd> a tab</div><div>Add the Session to the Tile Grid</div>
|
||||
<div><kbd>Right-click</kbd> the Tiles button</div><div>Choose Which Sessions to Tile</div>
|
||||
|
||||
@@ -40,17 +40,33 @@ function tileGridTracks(fr) {
|
||||
return fr.map((f) => `minmax(0, ${Math.round(f * 1000) / 1000}fr)`).join(` ${TILE_DIVIDER_PX}px `);
|
||||
}
|
||||
// Registry ids of the tile chords (DEFAULT_SHORTCUTS, app.js), and whether each
|
||||
// needs the grid open. The toggle applies wherever a grid could open.
|
||||
// needs the grid open. The toggle applies wherever a grid could open. The move
|
||||
// chords leave a text field its keys (`move`: see tileShortcutFor).
|
||||
const TILE_SHORTCUTS = {
|
||||
'toggle-tile-grid': { needsOpen: false },
|
||||
'focus-tile-left': { needsOpen: true, direction: 'left' },
|
||||
'focus-tile-right': { needsOpen: true, direction: 'right' },
|
||||
'focus-tile-up': { needsOpen: true, direction: 'up' },
|
||||
'focus-tile-down': { needsOpen: true, direction: 'down' },
|
||||
'move-tile-left': { needsOpen: true, move: 'left' },
|
||||
'move-tile-right': { needsOpen: true, move: 'right' },
|
||||
'move-tile-up': { needsOpen: true, move: 'up' },
|
||||
'move-tile-down': { needsOpen: true, move: 'down' },
|
||||
'remove-tile': { needsOpen: true },
|
||||
'zoom-tile': { needsOpen: true },
|
||||
};
|
||||
|
||||
/**
|
||||
* A text field other than a terminal's own input (xterm's helper textarea):
|
||||
* the rename input, the file editor, a settings field. Ctrl+Shift+Arrows
|
||||
* selects by word there, so the move chords leave it alone.
|
||||
*/
|
||||
function isTextFieldTarget(target) {
|
||||
if (!target || target.classList?.contains?.('xterm-helper-textarea')) return false;
|
||||
const tag = String(target.tagName || '').toUpperCase();
|
||||
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable === true;
|
||||
}
|
||||
|
||||
/** The grid's state. `has(id)` answers only while it is open. */
|
||||
class TileGridModel {
|
||||
constructor() {
|
||||
@@ -528,9 +544,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
* toggle applies while the grid is open, or where one could open AND the
|
||||
* per-device `showTileGridButton` setting is on: with it off (the default)
|
||||
* the chord is inert and reaches the terminal like any unbound key (owner
|
||||
* decision 6 in docs/tile-grid-plan.md). The focus,
|
||||
* decision 6 in docs/tile-grid-plan.md). The focus, move,
|
||||
* zoom and remove chords apply only while the grid is open, however it was
|
||||
* opened. Registry-aware (rebinds and disables in App Settings, Shortcuts).
|
||||
* opened (a move chord also while a tile is zoomed, as a no-op, so its keys
|
||||
* never reach the CLI; and never in a text field, whose keys they are).
|
||||
* Registry-aware (rebinds and disables in App Settings, Shortcuts).
|
||||
* The capture handler (app.js) dispatches it; every xterm key handler returns
|
||||
* false for it, so a chord that applies never reaches a PTY.
|
||||
*
|
||||
@@ -543,6 +561,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
for (const shortcut of this.getShortcutRegistry()) {
|
||||
const spec = TILE_SHORTCUTS[shortcut.id];
|
||||
if (!spec || shortcut.disabled || !this.matchesShortcutEvent(e, shortcut)) continue;
|
||||
if (spec.move && isTextFieldTarget(e.target)) continue;
|
||||
if (spec.needsOpen) return open ? shortcut.id : null;
|
||||
const enabled = this.loadAppSettingsFromStorage?.()?.showTileGridButton === true;
|
||||
return open || (enabled && this.canOpenTileGrid()) ? shortcut.id : null;
|
||||
@@ -558,6 +577,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
else if (id === 'remove-tile') this.removeFocusedTile();
|
||||
else if (id === 'zoom-tile') this.zoomTile(this._tileGrid?.focusedId);
|
||||
else if (spec.direction) this.focusTileInDirection(spec.direction);
|
||||
else if (spec.move) this.moveTileInDirection(spec.move);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -838,6 +858,19 @@ Object.assign(CodemanApp.prototype, {
|
||||
return this._reorderTiles(ids);
|
||||
},
|
||||
|
||||
/**
|
||||
* Move Tile Left/Right/Up/Down: the focused tile trades places with its
|
||||
* neighbour in that direction, the neighbour the Alt+Shift+Arrow focus
|
||||
* chords pick (tileInDirection). Focus stays on the moved tile. Nothing while
|
||||
* a tile is zoomed, or at an edge.
|
||||
*/
|
||||
moveTileInDirection(direction) {
|
||||
const grid = this._tileGrid;
|
||||
if (!grid?.open || grid.zoomedId || !grid.focusedId) return;
|
||||
const neighbor = window.CodemanTileGrid.tileInDirection(grid.ids, grid.focusedId, direction, grid.cols);
|
||||
if (neighbor) this._swapTiles(grid.focusedId, neighbor);
|
||||
},
|
||||
|
||||
/**
|
||||
* A tab dropped on a tile, or a tile dragged there by its header: a session
|
||||
* not yet tiled REPLACES that tile (same place; the replaced session keeps
|
||||
|
||||
Reference in New Issue
Block a user