From 1a80a5f4aff669b389f70ea8d25000991a6d0659 Mon Sep 17 00:00:00 2001 From: ChrisLi Date: Fri, 15 May 2026 16:39:12 +0800 Subject: [PATCH 1/3] feat: customizable tab navigation shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Ctrl+Shift+PageUp/PageDown to move the active tab left/right (with wrap-around), and expose all four tab-navigation actions (prev/next/move-left/move-right) for rebinding in Settings → Hotkeys. Bindings persist via localStorage; each action accepts multiple bindings and ships with the previous hardcoded defaults (Shift+ArrowUp/Down, Ctrl+PageUp/Down, Ctrl+Shift+PageUp/Down). Co-Authored-By: Claude Opus 4.7 --- src/App.tsx | 36 +++--- src/components/SettingsDialog.tsx | 177 +++++++++++++++++++++++++++++- src/stores/shortcuts.ts | 145 ++++++++++++++++++++++++ 3 files changed, 341 insertions(+), 17 deletions(-) create mode 100644 src/stores/shortcuts.ts diff --git a/src/App.tsx b/src/App.tsx index f304551..43e74d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import { api, type Connection } from "./ipc/api"; import { loadConnections } from "./stores/connections"; import { loadGeneral } from "./stores/general"; import { closeSearch, openSearch, searchTabId } from "./stores/search"; +import { actionFor } from "./stores/shortcuts"; import { C } from "./theme"; import { activeTab, @@ -31,6 +32,7 @@ import { markCwdTabId, newTabId, reconnectTabFromProfile, + reorderTabs, restoreTabs, setActiveTab, tabs, @@ -123,23 +125,18 @@ export default function App() { window.addEventListener("beforeunload", () => flushPersistedState()); - // Shift+ArrowUp/Down and Ctrl+PageUp/PageDown: cycle tabs. Registered in - // the capture phase so it fires before xterm's textarea handler — - // otherwise xterm swallows the event (translates it to a PTY escape - // sequence) and the tab bar only responds after the user clicks the - // sidebar to move focus. Skipped when focus is in a real text input / + // Tab cycle / move shortcuts (user-customizable; see stores/shortcuts.ts). + // Registered in the capture phase so it fires before xterm's textarea + // handler — otherwise xterm swallows the event (translates it to a PTY + // escape sequence) and the tab bar only responds after the user clicks + // the sidebar to move focus. Skipped when focus is in a real text input / // passthrough mode so the key can still extend selection / be forwarded // to the remote agent. window.addEventListener( "keydown", (e) => { - const shiftCombo = - e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && - (e.key === "ArrowUp" || e.key === "ArrowDown"); - const ctrlCombo = - e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && - (e.key === "PageUp" || e.key === "PageDown"); - if (!shiftCombo && !ctrlCombo) return; + const action = actionFor(e); + if (!action) return; if (isActiveTabPassthrough()) return; // xterm's own input is a hidden