Add configurable global hotkeys (#1305)

This commit is contained in:
ZacharyZcR
2026-08-24 01:10:19 +08:00
committed by GitHub
parent 9ee50d624a
commit cad7520c9f
7 changed files with 121 additions and 1 deletions
+77
View File
@@ -41,6 +41,12 @@ import { defaultSizes, SplitView, type RowColSizes } from "@/shell/SplitView";
import { renderTabContent } from "@/shell/tabUtils";
import { TabBar } from "@/shell/TabBar";
import { dispatchCtrlW, isShiftKey } from "@/lib/app-keyboard-shortcuts";
import { parseCustomKeybindings } from "@/api/open-tabs-api";
import { findMatchingKeybinding } from "@/lib/keybinding-match";
import type {
CustomKeybinding,
KeybindingActionType,
} from "@/types/keybindings";
// Shell surfaces that are not needed for first paint.
const CommandPalette = lazy(() =>
@@ -454,6 +460,7 @@ export function AppShell({
const tabsRef = useRef(tabs);
const activeTabIdRef = useRef(activeTabId);
const closeActiveTabRef = useRef<() => void>(() => {});
const globalKeybindingsRef = useRef<CustomKeybinding[]>([]);
const splitModeRef = useRef(splitMode);
const focusedPaneIndexRef = useRef<number | null>(null);
const paneContentElsRef = useRef<(HTMLDivElement | null)[]>(
@@ -463,6 +470,76 @@ export function AppShell({
useEffect(() => {
tabsRef.current = tabs;
}, [tabs]);
useEffect(() => {
let cancelled = false;
const load = () => {
getUserPreferences()
.then((prefs) => {
if (cancelled) return;
globalKeybindingsRef.current = parseCustomKeybindings(
prefs.customKeybindings,
).filter(
(binding) =>
binding.enabled &&
["nextTab", "previousTab", "openCommandPalette"].includes(
binding.action.type,
),
);
})
.catch(() => {});
};
load();
window.addEventListener("customKeybindingsChanged", load);
return () => {
cancelled = true;
window.removeEventListener("customKeybindingsChanged", load);
};
}, []);
useEffect(() => {
const runAction = (type: KeybindingActionType) => {
if (type === "openCommandPalette") {
setCommandPaletteOpen(true);
return;
}
const currentTabs = tabsRef.current;
if (currentTabs.length < 2) return;
const index = currentTabs.findIndex(
(tab) => tab.id === activeTabIdRef.current,
);
const offset = type === "nextTab" ? 1 : -1;
const next = (index + offset + currentTabs.length) % currentTabs.length;
setActiveTabId(currentTabs[next].id);
};
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.target instanceof Element &&
event.target.closest("[data-keybinding-recorder]")
)
return;
const binding = findMatchingKeybinding(
event,
globalKeybindingsRef.current,
);
if (!binding) return;
event.preventDefault();
event.stopPropagation();
runAction(binding.action.type);
};
const handleAction = (event: Event) =>
runAction(
(event as CustomEvent<{ type: KeybindingActionType }>).detail.type,
);
window.addEventListener("keydown", handleKeyDown, true);
window.addEventListener("termix:global-keybinding", handleAction);
return () => {
window.removeEventListener("keydown", handleKeyDown, true);
window.removeEventListener("termix:global-keybinding", handleAction);
};
}, []);
useEffect(() => {
activeTabIdRef.current = activeTabId;
}, [activeTabId]);
+10
View File
@@ -82,5 +82,15 @@ export function dispatchKeybindingAction(
});
return;
}
case "nextTab":
case "previousTab":
case "openCommandPalette": {
window.dispatchEvent(
new CustomEvent("termix:global-keybinding", {
detail: { type: action.type },
}),
);
return;
}
}
}
+3
View File
@@ -4057,6 +4057,9 @@
"actionSendControlCode": "Send Ctrl+letter signal",
"actionSendText": "Send text",
"actionRunSnippet": "Run existing snippet",
"actionNextTab": "Switch to next tab",
"actionPreviousTab": "Switch to previous tab",
"actionOpenCommandPalette": "Open quick search",
"controlCodeLabel": "Letter",
"textLabel": "Text to send",
"appendEnterLabel": "Press Enter after sending",
+13
View File
@@ -44,6 +44,9 @@ const ACTION_LABEL_KEYS: Record<KeybindingActionType, string> = {
sendControlCode: "newUi.sidebar.keybindings.actionSendControlCode",
sendText: "newUi.sidebar.keybindings.actionSendText",
runSnippet: "newUi.sidebar.keybindings.actionRunSnippet",
nextTab: "newUi.sidebar.keybindings.actionNextTab",
previousTab: "newUi.sidebar.keybindings.actionPreviousTab",
openCommandPalette: "newUi.sidebar.keybindings.actionOpenCommandPalette",
};
function generateId(): string {
@@ -450,6 +453,7 @@ export function KeybindingsDialog({
<span className="text-accent-brand">*</span>
</label>
<Input
data-keybinding-recorder
readOnly
value={
recording
@@ -497,6 +501,15 @@ export function KeybindingsDialog({
<option value="runSnippet">
{t("newUi.sidebar.keybindings.actionRunSnippet")}
</option>
<option value="nextTab">
{t("newUi.sidebar.keybindings.actionNextTab")}
</option>
<option value="previousTab">
{t("newUi.sidebar.keybindings.actionPreviousTab")}
</option>
<option value="openCommandPalette">
{t("newUi.sidebar.keybindings.actionOpenCommandPalette")}
</option>
</select>
{actionType === "paste" && (
<span className="text-xs text-muted-foreground">