implement global shortcuts with electron api
This commit is contained in:
+1
-1
@@ -40,7 +40,7 @@
|
|||||||
"@types/react": "19.2.9",
|
"@types/react": "19.2.9",
|
||||||
"@vencord/types": "^1.14.1",
|
"@vencord/types": "^1.14.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"electron": "^41.0.4",
|
"electron": "^41.1.1",
|
||||||
"electron-builder": "^26.8.1",
|
"electron-builder": "^26.8.1",
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.4",
|
||||||
"eslint": "^10.1.0",
|
"eslint": "^10.1.0",
|
||||||
|
|||||||
Generated
+47
-25
@@ -42,8 +42,8 @@ importers:
|
|||||||
specifier: ^17.3.1
|
specifier: ^17.3.1
|
||||||
version: 17.3.1
|
version: 17.3.1
|
||||||
electron:
|
electron:
|
||||||
specifier: ^41.0.4
|
specifier: ^41.1.1
|
||||||
version: 41.0.4
|
version: 41.1.1
|
||||||
electron-builder:
|
electron-builder:
|
||||||
specifier: ^26.8.1
|
specifier: ^26.8.1
|
||||||
version: 26.8.1(electron-builder-squirrel-windows@25.1.8)
|
version: 26.8.1(electron-builder-squirrel-windows@25.1.8)
|
||||||
@@ -467,8 +467,8 @@ packages:
|
|||||||
'@types/node@22.19.15':
|
'@types/node@22.19.15':
|
||||||
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
||||||
|
|
||||||
'@types/node@24.12.0':
|
'@types/node@24.12.2':
|
||||||
resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==}
|
resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}
|
||||||
|
|
||||||
'@types/node@25.5.0':
|
'@types/node@25.5.0':
|
||||||
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
|
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
|
||||||
@@ -1151,8 +1151,8 @@ packages:
|
|||||||
electron-updater@6.6.2:
|
electron-updater@6.6.2:
|
||||||
resolution: {integrity: sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==}
|
resolution: {integrity: sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==}
|
||||||
|
|
||||||
electron@41.0.4:
|
electron@41.1.1:
|
||||||
resolution: {integrity: sha512-rO08CxnAsAkKPFj3OZnxFkKrlnpSL3OCOewMDj5kaohVo++7e8hIT5Sl+tNl9WkNKiLvfZSW180ueA9s5zh9dg==}
|
resolution: {integrity: sha512-8bgvDhBjli+3Z2YCKgzzoBPh6391pr7Xv2h/tTJG4ETgvPvUxZomObbZLs31mUzYb6VrlcDDd9cyWyNKtPm3tA==}
|
||||||
engines: {node: '>= 12.20.55'}
|
engines: {node: '>= 12.20.55'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -1233,8 +1233,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint-plugin-import: '>=1.4.0'
|
eslint-plugin-import: '>=1.4.0'
|
||||||
|
|
||||||
eslint-import-resolver-node@0.3.9:
|
eslint-import-resolver-node@0.3.10:
|
||||||
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
|
resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==}
|
||||||
|
|
||||||
eslint-module-utils@2.12.1:
|
eslint-module-utils@2.12.1:
|
||||||
resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==}
|
resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==}
|
||||||
@@ -2109,6 +2109,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
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:
|
minimatch@3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
|
|
||||||
@@ -2228,6 +2232,10 @@ packages:
|
|||||||
node-api-version@0.2.1:
|
node-api-version@0.2.1:
|
||||||
resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==}
|
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:
|
node-gyp@11.5.0:
|
||||||
resolution: {integrity: sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==}
|
resolution: {integrity: sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
@@ -2512,15 +2520,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==}
|
resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==}
|
||||||
deprecated: https://github.com/lydell/resolve-url#deprecated
|
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:
|
resolve@2.0.0-next.5:
|
||||||
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
|
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
resolve@2.0.0-next.6:
|
||||||
|
resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
responselike@2.0.1:
|
responselike@2.0.1:
|
||||||
resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==}
|
resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==}
|
||||||
|
|
||||||
@@ -3461,7 +3469,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/node@24.12.0':
|
'@types/node@24.12.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
@@ -3697,7 +3705,7 @@ snapshots:
|
|||||||
js-yaml: 4.1.1
|
js-yaml: 4.1.1
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
lazy-val: 1.0.5
|
lazy-val: 1.0.5
|
||||||
minimatch: 10.2.4
|
minimatch: 10.2.5
|
||||||
resedit: 1.7.2
|
resedit: 1.7.2
|
||||||
sanitize-filename: 1.6.4
|
sanitize-filename: 1.6.4
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
@@ -4443,10 +4451,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
electron@41.0.4:
|
electron@41.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@electron/get': 2.0.3
|
'@electron/get': 2.0.3
|
||||||
'@types/node': 24.12.0
|
'@types/node': 24.12.2
|
||||||
extract-zip: 2.0.1
|
extract-zip: 2.0.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -4666,21 +4674,21 @@ snapshots:
|
|||||||
dependencies:
|
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-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:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
resolve: 1.22.11
|
resolve: 2.0.0-next.6
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@6.0.2)
|
'@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: 10.1.0(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.10
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -4694,8 +4702,8 @@ snapshots:
|
|||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 10.1.0(jiti@2.6.1)
|
eslint: 10.1.0(jiti@2.6.1)
|
||||||
eslint-import-resolver-node: 0.3.9
|
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.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))
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -5644,6 +5652,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.5
|
brace-expansion: 5.0.5
|
||||||
|
|
||||||
|
minimatch@10.2.5:
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 5.0.5
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 1.1.12
|
brace-expansion: 1.1.12
|
||||||
@@ -5771,6 +5783,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.4
|
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:
|
node-gyp@11.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
env-paths: 2.2.1
|
env-paths: 2.2.1
|
||||||
@@ -6092,15 +6111,18 @@ snapshots:
|
|||||||
|
|
||||||
resolve-url@0.2.1: {}
|
resolve-url@0.2.1: {}
|
||||||
|
|
||||||
resolve@1.22.11:
|
resolve@2.0.0-next.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
path-parse: 1.0.7
|
path-parse: 1.0.7
|
||||||
supports-preserve-symlinks-flag: 1.0.0
|
supports-preserve-symlinks-flag: 1.0.0
|
||||||
|
|
||||||
resolve@2.0.0-next.5:
|
resolve@2.0.0-next.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
|
node-exports-info: 1.6.0
|
||||||
|
object-keys: 1.1.1
|
||||||
path-parse: 1.0.7
|
path-parse: 1.0.7
|
||||||
supports-preserve-symlinks-flag: 1.0.0
|
supports-preserve-symlinks-flag: 1.0.0
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { app, BrowserWindow, nativeTheme } from "electron";
|
|||||||
|
|
||||||
import { DATA_DIR } from "./constants";
|
import { DATA_DIR } from "./constants";
|
||||||
import { createFirstLaunchTour } from "./firstLaunch";
|
import { createFirstLaunchTour } from "./firstLaunch";
|
||||||
|
import { registerKeyBinds } from "./keyBinds";
|
||||||
import { createWindows, mainWin } from "./mainWindow";
|
import { createWindows, mainWin } from "./mainWindow";
|
||||||
import { registerMediaPermissionsHandler } from "./mediaPermissions";
|
import { registerMediaPermissionsHandler } from "./mediaPermissions";
|
||||||
import { registerScreenShareHandler } from "./screenShare";
|
import { registerScreenShareHandler } from "./screenShare";
|
||||||
@@ -59,6 +60,9 @@ function init() {
|
|||||||
app.commandLine.appendSwitch("disable-smooth-scrolling");
|
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
|
// disable renderer backgrounding to prevent the app from unloading when in the background
|
||||||
// https://github.com/electron/electron/issues/2822
|
// https://github.com/electron/electron/issues/2822
|
||||||
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
|
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
|
||||||
@@ -115,6 +119,7 @@ function init() {
|
|||||||
|
|
||||||
registerScreenShareHandler();
|
registerScreenShareHandler();
|
||||||
registerMediaPermissionsHandler();
|
registerMediaPermissionsHandler();
|
||||||
|
registerKeyBinds();
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -9,6 +9,7 @@ import "./settings.css";
|
|||||||
import { classNameFactory } from "@vencord/types/api/Styles";
|
import { classNameFactory } from "@vencord/types/api/Styles";
|
||||||
import { BaseText, Divider, ErrorBoundary } from "@vencord/types/components";
|
import { BaseText, Divider, ErrorBoundary } from "@vencord/types/components";
|
||||||
import { ComponentType } from "react";
|
import { ComponentType } from "react";
|
||||||
|
import { KeyBindSettings } from "renderer/globalShortcuts/ShortcutSettings";
|
||||||
import { Settings, useSettings } from "renderer/settings";
|
import { Settings, useSettings } from "renderer/settings";
|
||||||
import { isMac, isWindows } from "renderer/utils";
|
import { isMac, isWindows } from "renderer/utils";
|
||||||
|
|
||||||
@@ -148,6 +149,7 @@ const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>>
|
|||||||
defaultValue: false
|
defaultValue: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
KeyBinds: [KeyBindSettings],
|
||||||
"Developer Options": [DeveloperOptionsButton]
|
"Developer Options": [DeveloperOptionsButton]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<AbortController | null>(null);
|
||||||
|
|
||||||
|
const actionId = React.useId();
|
||||||
|
const keyId = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cl("group")}>
|
||||||
|
<BaseText size="md" weight="medium" className={cl("label")} id={actionId}>
|
||||||
|
Action
|
||||||
|
</BaseText>
|
||||||
|
<BaseText size="md" weight="medium" className={cl("label")} id={keyId}>
|
||||||
|
Keybind
|
||||||
|
</BaseText>
|
||||||
|
|
||||||
|
{/* @ts-expect-error incorrect type for aria-labelledby */}
|
||||||
|
<Select
|
||||||
|
aria-labelledby={actionId}
|
||||||
|
options={Object.entries(ShortcutActions).map(([action, label]) => ({ label, value: action }))}
|
||||||
|
isSelected={o => o === keyBind.action}
|
||||||
|
select={o => setKeyBind({ ...keyBind, action: o })}
|
||||||
|
serialize={identity}
|
||||||
|
/>
|
||||||
|
<div className={cl("recorder")} aria-labelledby={keyId}>
|
||||||
|
<input
|
||||||
|
className={cl("record-input")}
|
||||||
|
type="text"
|
||||||
|
value={keyBind.key || "No Keybind Set"}
|
||||||
|
readOnly
|
||||||
|
disabled={!keyBind.key}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="record-button"
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
onClick={async () => {
|
||||||
|
if (isRecording) {
|
||||||
|
abortController?.abort();
|
||||||
|
setIsRecording(false);
|
||||||
|
setAbortController(null);
|
||||||
|
} else {
|
||||||
|
const controller = new AbortController();
|
||||||
|
setAbortController(controller);
|
||||||
|
setIsRecording(true);
|
||||||
|
const key = await recordKeybind(controller.signal).catch(() => null);
|
||||||
|
setIsRecording(false);
|
||||||
|
setAbortController(null);
|
||||||
|
if (key) setKeyBind({ ...keyBind, key });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isRecording ? "Cancel Recording" : "Record"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button size="iconOnly" variant="secondary" aria-label="Delete Keybind" onClick={deleteKeyBind}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Switch checked={keyBind.enabled} onChange={v => setKeyBind({ ...keyBind, enabled: v })} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KeyBindSettings() {
|
||||||
|
const [keyBinds, setKeyBinds] = useState<KeyBind[]>(Settings.store.keyBinds ?? []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{keyBinds.map((keyBind, index) => (
|
||||||
|
<KeyBindGroup
|
||||||
|
key={keyBind.id}
|
||||||
|
keyBind={keyBind}
|
||||||
|
setKeyBind={newKeyBind => {
|
||||||
|
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;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setKeyBinds(binds => [
|
||||||
|
...binds,
|
||||||
|
{
|
||||||
|
action: "unassigned",
|
||||||
|
key: "",
|
||||||
|
enabled: true,
|
||||||
|
id: crypto.randomUUID()
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add a Keybind
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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<string> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { SettingsRouter } from "@vencord/types/webpack/common";
|
|||||||
import { IpcCommands } from "shared/IpcEvents";
|
import { IpcCommands } from "shared/IpcEvents";
|
||||||
|
|
||||||
import { openScreenSharePicker } from "./components/ScreenSharePicker";
|
import { openScreenSharePicker } from "./components/ScreenSharePicker";
|
||||||
|
import { handleKeyBind } from "./globalShortcuts/keybindHandler";
|
||||||
|
|
||||||
type IpcCommandHandler = (data: any) => any;
|
type IpcCommandHandler = (data: any) => any;
|
||||||
|
|
||||||
@@ -52,3 +53,5 @@ onIpcCommand(IpcCommands.NAVIGATE_SETTINGS, () => {
|
|||||||
onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages);
|
onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages);
|
||||||
|
|
||||||
onIpcCommand(IpcCommands.SCREEN_SHARE_PICKER, data => openScreenSharePicker(data.screens, data.skipPicker));
|
onIpcCommand(IpcCommands.SCREEN_SHARE_PICKER, data => openScreenSharePicker(data.screens, data.skipPicker));
|
||||||
|
|
||||||
|
onIpcCommand(IpcCommands.HANDLE_KEY_BIND, handleKeyBind);
|
||||||
|
|||||||
@@ -83,5 +83,7 @@ export const enum IpcCommands {
|
|||||||
|
|
||||||
GET_LANGUAGES = "navigator.languages",
|
GET_LANGUAGES = "navigator.languages",
|
||||||
|
|
||||||
SCREEN_SHARE_PICKER = "screenshare:picker"
|
SCREEN_SHARE_PICKER = "screenshare:picker",
|
||||||
|
|
||||||
|
HANDLE_KEY_BIND = "keyBinds:handle"
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Rectangle } from "electron";
|
import type { Rectangle } from "electron";
|
||||||
|
import { KeyBind } from "renderer/globalShortcuts/ShortcutSettings";
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
discordBranch?: "stable" | "canary" | "ptb";
|
discordBranch?: "stable" | "canary" | "ptb";
|
||||||
@@ -46,6 +47,8 @@ export interface Settings {
|
|||||||
onlySpeakers?: boolean;
|
onlySpeakers?: boolean;
|
||||||
onlyDefaultSpeakers?: boolean;
|
onlyDefaultSpeakers?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
keyBinds?: KeyBind[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
|
|||||||
Reference in New Issue
Block a user