Compare commits

12 Commits
Author SHA1 Message Date
agent b16d313a91 Merge branch 'main' into global-shortcuts
# Conflicts:
#	src/renderer/components/settings/Settings.tsx
2026-09-11 11:38:04 +02:00
Vendicated 912d0bce79 Merge remote-tracking branch 'origin/main' into global-shortcuts 2026-07-21 15:41:10 +02:00
V b08091d55c Discard changes to pnpm-lock.yaml 2026-07-21 15:37:57 +02:00
V 49f466a569 Discard changes to package.json 2026-07-21 15:37:48 +02:00
Vendicated 797c02bdd0 Add Push To Talk binds 2026-05-10 15:07:46 +02:00
V 02f1e71e58 Merge branch 'main' into global-shortcuts 2026-05-10 14:22:31 +02:00
Vendicated e6954304f7 Merge remote-tracking branch 'origin/main' into global-shortcuts 2026-04-27 03:05:34 +02:00
V 876fbe5fba Merge branch 'main' into global-shortcuts 2026-04-21 22:05:32 +02:00
Vendicated 9d7ca67606 Add ipc method to invoke keybinds
e.g. create gnome keybind with following command:
`bash -c 'echo run-shortcut:toggleMute | nc -U $XDG_RUNTIME_DIR/vesktop.sock'`
2026-04-05 06:08:03 +02:00
Vendicated cf3667c151 fixes etc 2026-04-05 04:07:23 +02:00
Vendicated f3e01c2ba4 implement shortcuts and add cli trigger argument 2026-04-05 01:28:54 +02:00
Vendicated d179b621a7 implement global shortcuts with electron api 2026-04-04 23:10:11 +02:00
16 changed files with 665 additions and 5 deletions
+7 -1
View File
@@ -43,6 +43,12 @@ const options = {
description: "Set User-Agent to a specific operating system. May trigger anti-spam or break voice chat", description: "Set User-Agent to a specific operating system. May trigger anti-spam or break voice chat",
options: ["windows", "linux", "darwin"] options: ["windows", "linux", "darwin"]
}, },
"run-shortcut": {
type: "string",
description:
"Run a predefined shortcut action (for custom key binds). Vesktop has to be open for this to have any effect",
argumentName: "action"
},
repair: { repair: {
type: "boolean", type: "boolean",
description: "Repair the application by re-downloading the latest Vencord files" description: "Repair the application by re-downloading the latest Vencord files"
@@ -139,7 +145,7 @@ export function checkCommandLineForHelpOrVersion() {
app.exit(1); app.exit(1);
} }
if ("options" in def && !def.options?.includes(value as string)) { if ("options" in def && !def.options?.includes(value as any)) {
console.error(`Invalid value for --${name}: ${value}\nExpected one of: ${def.options.join(", ")}`); console.error(`Invalid value for --${name}: ${value}\nExpected one of: ${def.options.join(", ")}`);
app.exit(1); app.exit(1);
} }
+2
View File
@@ -13,3 +13,5 @@ export const AppEvents = new EventEmitter<{
userAssetChanged: [UserAssetType]; userAssetChanged: [UserAssetType];
setTrayVariant: ["tray" | "trayUnread"]; setTrayVariant: ["tray" | "trayUnread"];
}>(); }>();
export const appReady = new Promise<void>(resolve => AppEvents.once("appLoaded", () => resolve()));
+4 -1
View File
@@ -8,6 +8,7 @@ import { randomUUID } from "crypto";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { IpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
import { appReady } from "./events";
import { mainWin } from "./mainWindow"; import { mainWin } from "./mainWindow";
const resolvers = new Map<string, Record<"resolve" | "reject", (data: any) => void>>(); const resolvers = new Map<string, Record<"resolve" | "reject", (data: any) => void>>();
@@ -30,7 +31,9 @@ export interface IpcResponse {
* *
* You must add a handler for the message in the renderer process. * You must add a handler for the message in the renderer process.
*/ */
export function sendRendererCommand<T = any>(message: string, data?: any) { export async function sendRendererCommand<T = any>(message: string, data?: any) {
await appReady;
if (mainWin.isDestroyed()) { if (mainWin.isDestroyed()) {
console.warn("Main window is destroyed, cannot send IPC command:", message); console.warn("Main window is destroyed, cannot send IPC command:", message);
return Promise.reject(new Error("Main window is destroyed")); return Promise.reject(new Error("Main window is destroyed"));
+61
View File
@@ -0,0 +1,61 @@
/*
* 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 } from "electron";
import { rmSync } from "fs";
import { rm } from "fs/promises";
import { createServer, Server } from "net";
import { join } from "path";
import { IpcCommands } from "shared/IpcEvents";
import { sendRendererCommand } from "./ipcCommands";
const { env, platform } = process;
let server: Server;
const SOCKET_PATH =
platform === "win32"
? "\\\\?\\pipe\\vesktop.sock"
: join(env.XDG_RUNTIME_DIR || env.TMPDIR || env.TMP || env.TEMP || "/tmp", "vesktop.sock");
export async function initSocket() {
await rm(SOCKET_PATH, { force: true });
server = createServer(socket => {
socket.setEncoding("utf-8");
const reply = (message: string) => {
socket.write(message);
socket.end();
};
socket.on("data", async (data: string) => {
try {
const [command, ...args] = data.trim().split(":");
if (command === "run-shortcut") {
await sendRendererCommand(IpcCommands.KEY_BINDS_HANDLE, args[0]);
return;
}
reply("Unknown Command");
} catch (e) {
reply("Unknown Error");
}
});
});
server.listen(SOCKET_PATH);
server.on("error", err => {
console.error("Vesktop IPC Socket Error:", err);
});
}
app.on("will-quit", () => {
server?.close();
rmSync(SOCKET_PATH, { force: true });
});
+39
View File
@@ -0,0 +1,39 @@
/*
* 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() {
if (!app.isReady()) return;
globalShortcut.unregisterAll();
}
export async function registerKeyBinds() {
await app.whenReady();
unregisterKeyBinds();
let success = true;
for (const { action, enabled, key } of Settings.store.keyBinds || []) {
if (!enabled || !key || action === "unassigned") continue;
const registeredSuccessfully = globalShortcut.register(key, () =>
sendRendererCommand(IpcCommands.KEY_BINDS_HANDLE, action)
);
success &&= registeredSuccessfully;
}
sendRendererCommand(IpcCommands.KEY_BINDS_SET_STATUS, success);
}
Settings.addChangeListener("keyBinds", registerKeyBinds);
app.on("will-quit", unregisterKeyBinds);
+19 -1
View File
@@ -11,9 +11,15 @@ import "./userAssets";
import "./vesktopProtocol"; import "./vesktopProtocol";
import { app, BrowserWindow, nativeTheme } from "electron"; import { app, BrowserWindow, nativeTheme } from "electron";
import { IpcCommands } from "shared/IpcEvents";
// eslint-disable-next-line no-duplicate-imports
import { CommandLine } from "./cli";
import { DATA_DIR } from "./constants"; import { DATA_DIR } from "./constants";
import { createFirstLaunchTour } from "./firstLaunch"; import { createFirstLaunchTour } from "./firstLaunch";
import { sendRendererCommand } from "./ipcCommands";
import { initSocket } from "./ipcSocket";
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";
@@ -71,6 +77,9 @@ function init() {
// Supposed to be fixed already according to comments there, but it's just not lol, I can repro on Electron 43.0.0 // Supposed to be fixed already according to comments there, but it's just not lol, I can repro on Electron 43.0.0
// when moving the window from my main monitor (HDR - not sure if this is relevant lol) to second monitor (SDR) and back // when moving the window from my main monitor (HDR - not sure if this is relevant lol) to second monitor (SDR) and back
disabledFeatures.add("WaylandWpColorManagerV1"); disabledFeatures.add("WaylandWpColorManagerV1");
// Enable Wayland Portal for global shortcuts
enabledFeatures.add("GlobalShortcutsPortal");
} }
disabledFeatures.forEach(feat => enabledFeatures.delete(feat)); disabledFeatures.forEach(feat => enabledFeatures.delete(feat));
@@ -105,6 +114,8 @@ function init() {
registerScreenShareHandler(); registerScreenShareHandler();
registerMediaPermissionsHandler(); registerMediaPermissionsHandler();
registerKeyBinds();
initSocket();
bootstrap(); bootstrap();
@@ -114,7 +125,7 @@ function init() {
}); });
} }
if (!app.requestSingleInstanceLock({ IS_DEV })) { if (!app.requestSingleInstanceLock({ IS_DEV, args: CommandLine })) {
if (IS_DEV) { if (IS_DEV) {
console.log("Vesktop is already running. Quitting previous instance..."); console.log("Vesktop is already running. Quitting previous instance...");
init(); init();
@@ -144,6 +155,13 @@ app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit(); if (process.platform !== "darwin") app.quit();
}); });
app.on("second-instance", (_e, _argv, _cwd, data) => {
const { args } = data as { args: typeof CommandLine; IS_DEV: boolean };
const { "run-shortcut": shortcut } = args.values;
if (shortcut) sendRendererCommand(IpcCommands.KEY_BINDS_HANDLE, shortcut);
});
// Sets the WebRTC IP handling policy for all current and future windows. // Sets the WebRTC IP handling policy for all current and future windows.
// Switching to "default_public_and_private_interfaces" may fix calls stuck at "DTLS Connecting" when using VPNs, Tailscale, etc. // Switching to "default_public_and_private_interfaces" may fix calls stuck at "DTLS Connecting" when using VPNs, Tailscale, etc.
// https://github.com/Vencord/Vesktop/issues/876 // https://github.com/Vencord/Vesktop/issues/876
@@ -10,6 +10,7 @@ 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 { WebRTCIPHandlingPolicyPicker } from "renderer/components/settings/WebRTCIPHandlingPolicyPicker"; import { WebRTCIPHandlingPolicyPicker } from "renderer/components/settings/WebRTCIPHandlingPolicyPicker";
import { KeybindsButton } from "renderer/globalShortcuts/ShortcutSettings";
import { getValueAndOnChange, Settings, useSettings } from "renderer/settings"; import { getValueAndOnChange, Settings, useSettings } from "renderer/settings";
import { isMac } from "renderer/utils"; import { isMac } from "renderer/utils";
@@ -122,7 +123,8 @@ const SettingsOptions: Record<string, Array<BooleanSetting | SettingsComponent>>
key: "disableSmoothScroll", key: "disableSmoothScroll",
title: "Disable smooth scrolling", title: "Disable smooth scrolling",
description: "Disables smooth scrolling" description: "Disables smooth scrolling"
} },
KeybindsButton
], ],
Notifications: [ Notifications: [
NotificationBadgeToggle, NotificationBadgeToggle,
@@ -0,0 +1,55 @@
.vcd-shortcuts-container {
display: flex;
flex-direction: column;
gap: 1em;
padding-block: 1em;
}
.vcd-shortcuts-group {
display: grid;
gap: 0.25em 0.5em;
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: 3px 4px 3px 12px;
box-sizing: border-box;
}
.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,183 @@
/*
* 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, Card, DeleteIcon, Switch } from "@vencord/types/components";
import { classNameFactory, identity, openModal } from "@vencord/types/utils";
import { Modal, React, Select, useState } from "@vencord/types/webpack/common";
import { SettingsComponent } from "renderer/components/settings/Settings";
import { onIpcCommand } from "renderer/ipcCommands";
import { reactiveValue } from "renderer/reactiveState";
import { Settings } from "renderer/settings";
import { IpcCommands } from "shared/IpcEvents";
import { recordKeybind } from "./recordKeybind";
const cl = classNameFactory("vcd-shortcuts-");
const ShortcutActions = {
unassigned: "Unassigned",
mute: "Mute",
unmute: "Unmute",
toggleMute: "Toggle Mute",
toggleDeafen: "Toggle Deafen",
toggleStreamerMode: "Toggle Streamer Mode",
pushToTalkNormalToggle: "Push-to-Talk (Normal) Toggle",
pushToTalkNormalStart: "Push-to-Talk (Normal) Start",
pushToTalkNormalStop: "Push-to-Talk (Normal) Stop",
pushToTalkPriorityToggle: "Push-to-Talk (Priority) Toggle",
pushToTalkPriorityStart: "Push-to-Talk (Priority) Start",
pushToTalkPriorityStop: "Push-to-Talk (Priority) Stop",
disconnectFromVoiceChannel: "Disconnect from VC"
} as const;
export type ShortcutAction = keyof typeof ShortcutActions;
const shortcutStatus = reactiveValue(true);
onIpcCommand(IpcCommands.KEY_BINDS_SET_STATUS, status => (shortcutStatus.value = status));
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 ?? []);
const shortcutsStatus = shortcutStatus.use();
return (
<div className={cl("container")}>
{shortcutsStatus === false && (
<Card variant="danger">
<BaseText size="md" weight="medium">
Some of your keybinds failed to register.
</BaseText>
</Card>
)}
{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>
);
}
export function openKeybindsModal() {
openModal(props => (
<Modal {...props} size="lg" title="Keybinds">
<KeyBindSettings />
</Modal>
));
}
export const KeybindsButton: SettingsComponent = () => {
return <Button onClick={() => openKeybindsModal()}>Configure Keybinds</Button>;
};
@@ -0,0 +1,89 @@
/*
* 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 { waitFor } from "@vencord/types/webpack";
import { FluxDispatcher } from "@vencord/types/webpack/common";
import { ShortcutAction } from "./ShortcutSettings";
const logger = new Logger("VesktopKeyBindHandler");
interface KeyBindAction {
onTrigger(...args: any[]): void;
isPressed?: boolean;
}
let KeyBindActions: Record<string, KeyBindAction>;
waitFor(["dispatch", "subscribe"], FluxDispatcher => {
FluxDispatcher.subscribe("KEYBINDS_REGISTER_GLOBAL_KEYBIND_ACTIONS", event => {
KeyBindActions = event.keybinds;
});
});
export function handleKeyBind(keyBind: ShortcutAction) {
logger.debug(`Handling key bind action: ${keyBind}`);
if (!KeyBindActions) {
logger.error("Failed to retrieve KeyBindActions, cannot handle key bind");
return;
}
switch (keyBind) {
case "unassigned":
break;
case "mute":
case "unmute":
FluxDispatcher.dispatch({
type: "AUDIO_SET_SELF_MUTE",
context: "default",
syncRemote: true,
playSoundEffect: true,
mute: keyBind === "mute"
});
break;
case "toggleMute":
KeyBindActions.TOGGLE_MUTE.onTrigger();
break;
case "toggleDeafen":
KeyBindActions.TOGGLE_DEAFEN.onTrigger();
break;
case "toggleStreamerMode":
KeyBindActions.TOGGLE_STREAMER_MODE.onTrigger();
break;
case "disconnectFromVoiceChannel":
KeyBindActions.DISCONNECT_FROM_VOICE_CHANNEL.onTrigger();
break;
case "pushToTalkNormalStart":
case "pushToTalkNormalStop":
case "pushToTalkNormalToggle": {
const isPressed = keyBind.includes("Toggle")
? !KeyBindActions.PUSH_TO_TALK.isPressed
: keyBind.includes("Start");
KeyBindActions.PUSH_TO_TALK.onTrigger(isPressed, "default");
break;
}
case "pushToTalkPriorityStart":
case "pushToTalkPriorityStop":
case "pushToTalkPriorityToggle": {
const isPressed = keyBind.includes("Toggle")
? !KeyBindActions.PUSH_TO_TALK_PRIORITY.isPressed
: keyBind.includes("Start");
KeyBindActions.PUSH_TO_TALK_PRIORITY.onTrigger(isPressed, "default");
break;
}
default:
logger.warn(`Unknown key bind action: ${keyBind}`);
}
}
@@ -0,0 +1,126 @@
/*
* 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, { capture: true });
signal?.removeEventListener("abort", onAbort);
}
function onAbort() {
cleanup();
reject(new DOMException("Recording aborted", "AbortError"));
}
function onKeyDown(event: KeyboardEvent) {
event.preventDefault();
event.stopImmediatePropagation();
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, { capture: true });
});
}
+3
View File
@@ -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.KEY_BINDS_HANDLE, handleKeyBind);
+27
View File
@@ -0,0 +1,27 @@
/*
* 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 { Button, ErrorBoundary } from "@vencord/types/components";
import { openKeybindsModal } from "renderer/globalShortcuts/ShortcutSettings";
import { addPatch } from "./shared";
addPatch({
patches: [
{
find: "#{intl::KEYBIND_IN_BROSWER_NOTICE}",
replacement: {
match: /:(.{0,10}jsx.{0,50}messageType:.{0,100}#{intl::KEYBIND_IN_BROSWER_NOTICE})/,
replace: ":true?$self.renderKeybindsButton():$1"
}
}
],
renderKeybindsButton: ErrorBoundary.wrap(
() => <Button onClick={() => openKeybindsModal()}>Configure Vesktop Keybinds</Button>,
{ noop: true }
)
});
+40
View File
@@ -0,0 +1,40 @@
/*
* 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 { useForceUpdater } from "@vencord/types/utils";
import { useEffect } from "@vencord/types/webpack/common";
interface ReactiveValue<T> {
value: T;
use(): T;
}
export function reactiveValue<T>(initialValue: T): ReactiveValue<T>;
export function reactiveValue<T>(initialValue?: T): ReactiveValue<T | undefined>;
export function reactiveValue<T>(initialValue?: T) {
const changeListeners = new Set<() => void>();
let value = initialValue;
return {
get value() {
return value;
},
set value(newValue) {
value = newValue;
changeListeners.forEach(cb => cb());
},
use() {
const update = useForceUpdater();
useEffect(() => {
changeListeners.add(update);
return () => void changeListeners.delete(update);
}, []);
return value;
}
};
}
+4 -1
View File
@@ -83,5 +83,8 @@ export const enum IpcCommands {
GET_LANGUAGES = "navigator.languages", GET_LANGUAGES = "navigator.languages",
SCREEN_SHARE_PICKER = "screenshare:picker" SCREEN_SHARE_PICKER = "screenshare:picker",
KEY_BINDS_HANDLE = "keyBinds:handle",
KEY_BINDS_SET_STATUS = "keyBinds:setStatus"
} }
+3
View File
@@ -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";
@@ -54,6 +55,8 @@ export interface Settings {
onlySpeakers?: boolean; onlySpeakers?: boolean;
onlyDefaultSpeakers?: boolean; onlyDefaultSpeakers?: boolean;
}; };
keyBinds?: KeyBind[];
} }
export interface State { export interface State {