From d179b621a70b8ca58dcc9ca6a9ff552f4850add5 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 4 Apr 2026 23:10:11 +0200 Subject: [PATCH] implement global shortcuts with electron api --- package.json | 2 +- pnpm-lock.yaml | 72 ++++++--- src/main/index.ts | 5 + src/main/keyBinds.ts | 35 ++++ src/renderer/components/settings/Settings.tsx | 2 + .../globalShortcuts/ShortcutSettings.css | 47 ++++++ .../globalShortcuts/ShortcutSettings.tsx | 149 ++++++++++++++++++ .../globalShortcuts/keybindHandler.ts | 34 ++++ src/renderer/globalShortcuts/recordKeybind.ts | 125 +++++++++++++++ src/renderer/ipcCommands.ts | 3 + src/shared/IpcEvents.ts | 4 +- src/shared/settings.d.ts | 3 + 12 files changed, 454 insertions(+), 27 deletions(-) create mode 100644 src/main/keyBinds.ts create mode 100644 src/renderer/globalShortcuts/ShortcutSettings.css create mode 100644 src/renderer/globalShortcuts/ShortcutSettings.tsx create mode 100644 src/renderer/globalShortcuts/keybindHandler.ts create mode 100644 src/renderer/globalShortcuts/recordKeybind.ts diff --git a/package.json b/package.json index fa3abd6..7fca83a 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@types/react": "19.2.9", "@vencord/types": "^1.14.1", "dotenv": "^17.3.1", - "electron": "^41.0.4", + "electron": "^41.1.1", "electron-builder": "^26.8.1", "esbuild": "^0.27.4", "eslint": "^10.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5ca45f..b9db158 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: ^17.3.1 version: 17.3.1 electron: - specifier: ^41.0.4 - version: 41.0.4 + specifier: ^41.1.1 + version: 41.1.1 electron-builder: specifier: ^26.8.1 version: 26.8.1(electron-builder-squirrel-windows@25.1.8) @@ -467,8 +467,8 @@ packages: '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} - '@types/node@24.12.0': - resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} '@types/node@25.5.0': resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} @@ -1151,8 +1151,8 @@ packages: electron-updater@6.6.2: resolution: {integrity: sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==} - electron@41.0.4: - resolution: {integrity: sha512-rO08CxnAsAkKPFj3OZnxFkKrlnpSL3OCOewMDj5kaohVo++7e8hIT5Sl+tNl9WkNKiLvfZSW180ueA9s5zh9dg==} + electron@41.1.1: + resolution: {integrity: sha512-8bgvDhBjli+3Z2YCKgzzoBPh6391pr7Xv2h/tTJG4ETgvPvUxZomObbZLs31mUzYb6VrlcDDd9cyWyNKtPm3tA==} engines: {node: '>= 12.20.55'} hasBin: true @@ -1233,8 +1233,8 @@ packages: peerDependencies: eslint-plugin-import: '>=1.4.0' - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} eslint-module-utils@2.12.1: resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} @@ -2109,6 +2109,10 @@ packages: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2228,6 +2232,10 @@ packages: node-api-version@0.2.1: resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + node-gyp@11.5.0: resolution: {integrity: sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -2512,15 +2520,15 @@ packages: resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} deprecated: https://github.com/lydell/resolve-url#deprecated - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true - resolve@2.0.0-next.5: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + engines: {node: '>= 0.4'} + hasBin: true + responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} @@ -3461,7 +3469,7 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@24.12.0': + '@types/node@24.12.2': dependencies: undici-types: 7.16.0 @@ -3697,7 +3705,7 @@ snapshots: js-yaml: 4.1.1 json5: 2.2.3 lazy-val: 1.0.5 - minimatch: 10.2.4 + minimatch: 10.2.5 resedit: 1.7.2 sanitize-filename: 1.6.4 semver: 7.7.4 @@ -4443,10 +4451,10 @@ snapshots: transitivePeerDependencies: - supports-color - electron@41.0.4: + electron@41.1.1: dependencies: '@electron/get': 2.0.3 - '@types/node': 24.12.0 + '@types/node': 24.12.2 extract-zip: 2.0.1 transitivePeerDependencies: - supports-color @@ -4666,21 +4674,21 @@ snapshots: dependencies: eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.1.0(jiti@2.6.1)) - eslint-import-resolver-node@0.3.9: + eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 is-core-module: 2.16.1 - resolve: 1.22.11 + resolve: 2.0.0-next.6 transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint@10.1.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.1.0(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color @@ -4694,8 +4702,8 @@ snapshots: debug: 3.2.7 doctrine: 2.1.0 eslint: 10.1.0(jiti@2.6.1) - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.9)(eslint@10.1.0(jiti@2.6.1)) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint@10.1.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5644,6 +5652,10 @@ snapshots: dependencies: brace-expansion: 5.0.5 + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -5771,6 +5783,13 @@ snapshots: dependencies: semver: 7.7.4 + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + node-gyp@11.5.0: dependencies: env-paths: 2.2.1 @@ -6092,15 +6111,18 @@ snapshots: resolve-url@0.2.1: {} - resolve@1.22.11: + resolve@2.0.0-next.5: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.5: + resolve@2.0.0-next.6: dependencies: + es-errors: 1.3.0 is-core-module: 2.16.1 + node-exports-info: 1.6.0 + object-keys: 1.1.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 diff --git a/src/main/index.ts b/src/main/index.ts index 00dec10..28a08e8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -14,6 +14,7 @@ import { app, BrowserWindow, nativeTheme } from "electron"; import { DATA_DIR } from "./constants"; import { createFirstLaunchTour } from "./firstLaunch"; +import { registerKeyBinds } from "./keyBinds"; import { createWindows, mainWin } from "./mainWindow"; import { registerMediaPermissionsHandler } from "./mediaPermissions"; import { registerScreenShareHandler } from "./screenShare"; @@ -59,6 +60,9 @@ function init() { app.commandLine.appendSwitch("disable-smooth-scrolling"); } + // Enable Wayland Portal for global shortcuts + enabledFeatures.add("GlobalShortcutsPortal"); + // disable renderer backgrounding to prevent the app from unloading when in the background // https://github.com/electron/electron/issues/2822 // https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling @@ -115,6 +119,7 @@ function init() { registerScreenShareHandler(); registerMediaPermissionsHandler(); + registerKeyBinds(); bootstrap(); diff --git a/src/main/keyBinds.ts b/src/main/keyBinds.ts new file mode 100644 index 0000000..77cd174 --- /dev/null +++ b/src/main/keyBinds.ts @@ -0,0 +1,35 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2026 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { app, globalShortcut } from "electron"; +import { IpcCommands } from "shared/IpcEvents"; + +import { sendRendererCommand } from "./ipcCommands"; +import { Settings } from "./settings"; + +function unregisterKeyBinds() { + globalShortcut.unregisterAll(); +} + +export async function registerKeyBinds() { + await app.whenReady(); + + unregisterKeyBinds(); + + for (const { action, enabled, key } of Settings.store.keyBinds || []) { + if (!enabled || !key || action === "unassigned") continue; + + console.log( + key, + "SUCCESS", + globalShortcut.register(key, () => sendRendererCommand(IpcCommands.HANDLE_KEY_BIND, action)) + ); + } +} + +Settings.addChangeListener("keyBinds", registerKeyBinds); + +app.on("will-quit", unregisterKeyBinds); diff --git a/src/renderer/components/settings/Settings.tsx b/src/renderer/components/settings/Settings.tsx index 82a58db..b3be64a 100644 --- a/src/renderer/components/settings/Settings.tsx +++ b/src/renderer/components/settings/Settings.tsx @@ -9,6 +9,7 @@ import "./settings.css"; import { classNameFactory } from "@vencord/types/api/Styles"; import { BaseText, Divider, ErrorBoundary } from "@vencord/types/components"; import { ComponentType } from "react"; +import { KeyBindSettings } from "renderer/globalShortcuts/ShortcutSettings"; import { Settings, useSettings } from "renderer/settings"; import { isMac, isWindows } from "renderer/utils"; @@ -148,6 +149,7 @@ const SettingsOptions: Record> defaultValue: false } ], + KeyBinds: [KeyBindSettings], "Developer Options": [DeveloperOptionsButton] }; diff --git a/src/renderer/globalShortcuts/ShortcutSettings.css b/src/renderer/globalShortcuts/ShortcutSettings.css new file mode 100644 index 0000000..fdbfd24 --- /dev/null +++ b/src/renderer/globalShortcuts/ShortcutSettings.css @@ -0,0 +1,47 @@ +.vcd-shortcuts-group { + display: grid; + gap: 0.25em 1em; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto auto; + align-items: center; +} + +.vcd-shortcuts-label:first-child { + grid-column: 1 / 2; +} + +.vcd-shortcuts-label:nth-child(2) { + grid-column: 2 / 5; +} + +.vcd-shortcuts-recorder { + display: flex; + background-color: var(--input-background-default); + border: 1px solid; + border-color: var(--input-border-default); + border-radius: var(--radius-sm); + width: 100%; + min-height: calc(var(--select-option-height) - 2px); + padding: 0.25em; +} + +.vcd-shortcuts-record-input { + background-color: transparent; + border: none; + flex: 1 1 0; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text-strong); + font-weight: 600; + + &:disabled { + color: var(--text-muted); + font-weight: 400; + } +} + +.vcd-shortcuts-record-button { + flex: 0 0 auto; + white-space: nowrap; + min-width: max-content; +} \ No newline at end of file diff --git a/src/renderer/globalShortcuts/ShortcutSettings.tsx b/src/renderer/globalShortcuts/ShortcutSettings.tsx new file mode 100644 index 0000000..378598d --- /dev/null +++ b/src/renderer/globalShortcuts/ShortcutSettings.tsx @@ -0,0 +1,149 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2026 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./ShortcutSettings.css"; + +import { BaseText, Button, DeleteIcon, Switch } from "@vencord/types/components"; +import { classNameFactory, identity } from "@vencord/types/utils"; +import { React, Select, useState } from "@vencord/types/webpack/common"; +import { Settings } from "renderer/settings"; + +import { recordKeybind } from "./recordKeybind"; + +const cl = classNameFactory("vcd-shortcuts-"); + +const ShortcutActions = { + unassigned: "Unassigned", + toggleMute: "Toggle Mute", + toggleDeafen: "Toggle Deafen", + toggleStreamerMode: "Toggle Streamer Mode", + toggleCamera: "Toggle Camera", + toggleScreenShare: "Toggle Screen Share", + disconnectFromVoiceChannel: "Disconnect from Voice Channel" +} as const; + +export type ShortcutAction = keyof typeof ShortcutActions; + +export interface KeyBind { + id: string; + enabled: boolean; + action: ShortcutAction; + key: string; +} + +function KeyBindGroup({ + keyBind, + setKeyBind, + deleteKeyBind +}: { + keyBind: KeyBind; + setKeyBind(keyBind: KeyBind): void; + deleteKeyBind(): void; +}) { + const [isRecording, setIsRecording] = useState(false); + const [abortController, setAbortController] = useState(null); + + const actionId = React.useId(); + const keyId = React.useId(); + + return ( +
+ + Action + + + Keybind + + + {/* @ts-expect-error incorrect type for aria-labelledby */} + + +
+ + + + setKeyBind({ ...keyBind, enabled: v })} /> + + ); +} + +export function KeyBindSettings() { + const [keyBinds, setKeyBinds] = useState(Settings.store.keyBinds ?? []); + + return ( +
+ {keyBinds.map((keyBind, index) => ( + { + const newKeyBinds = [...keyBinds]; + newKeyBinds[index] = newKeyBind; + setKeyBinds(newKeyBinds); + Settings.store.keyBinds = newKeyBinds; + }} + deleteKeyBind={() => { + const newKeyBinds = keyBinds.filter((_, i) => i !== index); + setKeyBinds(newKeyBinds); + Settings.store.keyBinds = newKeyBinds; + }} + /> + ))} + +
+ ); +} diff --git a/src/renderer/globalShortcuts/keybindHandler.ts b/src/renderer/globalShortcuts/keybindHandler.ts new file mode 100644 index 0000000..c6cbe4b --- /dev/null +++ b/src/renderer/globalShortcuts/keybindHandler.ts @@ -0,0 +1,34 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2026 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Logger } from "@vencord/types/utils"; + +import { ShortcutAction } from "./ShortcutSettings"; + +const logger = new Logger("VesktopKeyBindHandler"); + +export function handleKeyBind(keyBind: ShortcutAction) { + logger.debug(`Handling key bind action: ${keyBind}`); + + switch (keyBind) { + case "toggleMute": + break; + case "toggleDeafen": + break; + case "toggleStreamerMode": + break; + case "toggleCamera": + break; + case "toggleScreenShare": + break; + case "disconnectFromVoiceChannel": + break; + case "unassigned": + break; + default: + logger.warn(`Unknown key bind action: ${keyBind}`); + } +} diff --git a/src/renderer/globalShortcuts/recordKeybind.ts b/src/renderer/globalShortcuts/recordKeybind.ts new file mode 100644 index 0000000..d356980 --- /dev/null +++ b/src/renderer/globalShortcuts/recordKeybind.ts @@ -0,0 +1,125 @@ +/* + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2026 Vendicated and Vesktop contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +const MODIFIER_KEYS = new Set(["Control", "Shift", "Alt", "Meta", "Dead"]); + +const VALID_PUNCTUATION = new Set(')!@#$%^&*(;:=<,_-.>?/~`{][|\\}"'); + +// Codes whose accelerator name matches the code value exactly +const PASSTHROUGH_CODES = new Set([ + "Space", + "Tab", + "Backspace", + "Delete", + "Insert", + "Home", + "End", + "PageUp", + "PageDown", + "PrintScreen", + "Escape" +]); + +// Codes that need an explicit rename +const CODE_MAP: Record = { + Enter: "Return", + NumpadEnter: "Return", + NumpadDecimal: "numdec", + NumpadAdd: "numadd", + NumpadSubtract: "numsub", + NumpadMultiply: "nummult", + NumpadDivide: "numdiv", + CapsLock: "Capslock", + NumLock: "Numlock", + ScrollLock: "Scrolllock", + ArrowUp: "Up", + ArrowDown: "Down", + ArrowLeft: "Left", + ArrowRight: "Right" +}; + +const MEDIA_KEY_MAP: Record = { + AudioVolumeUp: "VolumeUp", + AudioVolumeDown: "VolumeDown", + AudioVolumeMute: "VolumeMute", + MediaTrackNext: "MediaNextTrack", + MediaTrackPrevious: "MediaPreviousTrack", + MediaStop: "MediaStop", + MediaPlayPause: "MediaPlayPause" +}; + +function codeToAcceleratorKey(event: KeyboardEvent): string | null { + const { code, key } = event; + + // Letters: KeyA -> A + const letterMatch = /^Key([A-Z])$/.exec(code); + if (letterMatch) return letterMatch[1]; + + // Digits: Digit0 -> 0 + const digitMatch = /^Digit(\d)$/.exec(code); + if (digitMatch) return digitMatch[1]; + + // Function keys: F1-F24 + if (/^F([1-9]|1\d|2[0-4])$/.test(code)) return code; + + // Numpad digits: Numpad0 -> num0 + const numpadDigitMatch = /^Numpad(\d)$/.exec(code); + if (numpadDigitMatch) return "num" + numpadDigitMatch[1]; + + if (PASSTHROUGH_CODES.has(code)) return code; + if (code in CODE_MAP) return CODE_MAP[code]; + + // Media / audio keys (identified by event.key) + if (key in MEDIA_KEY_MAP) return MEDIA_KEY_MAP[key]; + + // Punctuation keys + if (key === "+") return "Plus"; + if (VALID_PUNCTUATION.has(key)) return key; + + return null; +} + +export async function recordKeybind(signal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(new DOMException("Recording aborted", "AbortError")); + return; + } + + function cleanup() { + document.removeEventListener("keydown", onKeyDown); + signal?.removeEventListener("abort", onAbort); + } + + function onAbort() { + cleanup(); + reject(new DOMException("Recording aborted", "AbortError")); + } + + function onKeyDown(event: KeyboardEvent) { + event.preventDefault(); + + if (MODIFIER_KEYS.has(event.key)) return; + + const key = codeToAcceleratorKey(event); + if (key === null) return; + + cleanup(); + + const parts: string[] = []; + if (event.ctrlKey) parts.push("Ctrl"); + if (event.shiftKey) parts.push("Shift"); + if (event.altKey) parts.push("Alt"); + if (event.metaKey) parts.push("Super"); + parts.push(key); + + resolve(parts.join("+")); + } + + signal?.addEventListener("abort", onAbort, { once: true }); + document.addEventListener("keydown", onKeyDown); + }); +} diff --git a/src/renderer/ipcCommands.ts b/src/renderer/ipcCommands.ts index be434c5..9625e1a 100644 --- a/src/renderer/ipcCommands.ts +++ b/src/renderer/ipcCommands.ts @@ -8,6 +8,7 @@ import { SettingsRouter } from "@vencord/types/webpack/common"; import { IpcCommands } from "shared/IpcEvents"; import { openScreenSharePicker } from "./components/ScreenSharePicker"; +import { handleKeyBind } from "./globalShortcuts/keybindHandler"; type IpcCommandHandler = (data: any) => any; @@ -52,3 +53,5 @@ onIpcCommand(IpcCommands.NAVIGATE_SETTINGS, () => { onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages); onIpcCommand(IpcCommands.SCREEN_SHARE_PICKER, data => openScreenSharePicker(data.screens, data.skipPicker)); + +onIpcCommand(IpcCommands.HANDLE_KEY_BIND, handleKeyBind); diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 075ec14..75e9941 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -83,5 +83,7 @@ export const enum IpcCommands { GET_LANGUAGES = "navigator.languages", - SCREEN_SHARE_PICKER = "screenshare:picker" + SCREEN_SHARE_PICKER = "screenshare:picker", + + HANDLE_KEY_BIND = "keyBinds:handle" } diff --git a/src/shared/settings.d.ts b/src/shared/settings.d.ts index 5b8fd6f..719588f 100644 --- a/src/shared/settings.d.ts +++ b/src/shared/settings.d.ts @@ -5,6 +5,7 @@ */ import type { Rectangle } from "electron"; +import { KeyBind } from "renderer/globalShortcuts/ShortcutSettings"; export interface Settings { discordBranch?: "stable" | "canary" | "ptb"; @@ -46,6 +47,8 @@ export interface Settings { onlySpeakers?: boolean; onlyDefaultSpeakers?: boolean; }; + + keyBinds?: KeyBind[]; } export interface State {