WaylandKeybinds: reliable Discord global keybinds on GNOME Wayland
Vencord plugin for the Discord desktop app. Toggles fire on key press, push-to-talk infers releases GNOME drops (re-registration probe), and GNOME's trigger descriptions are matched correctly. Includes basis-path tests, a patch checker against the live Discord bundle, the design spec and the README assets.
This commit is contained in:
commit
a3a9149184
10 files changed
+1514
No files matched your search
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Vencord, a Discord client mod
|
||||
* Copyright (c) 2026 Evandro
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { IS_LINUX } from "@utils/constants";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import definePlugin from "@utils/types";
|
||||
import { findStoreLazy } from "@webpack";
|
||||
|
||||
import { adjustOptions, Callback, createDispatcher, Dispatcher, fixPortalState, KeyEventOptions, planRegistration, PortalState, Scheduler } from "./logic";
|
||||
import { PATCHES } from "./patches";
|
||||
|
||||
type Shortcut = [number, number, ...unknown[]][];
|
||||
|
||||
// Discord's own native keybind bridge (the object the KeybindsStore registers through)
|
||||
interface Bridge {
|
||||
inputEventRegister(id: number, shortcut: Shortcut, callback: Callback, options: KeyEventOptions): void;
|
||||
inputEventUnregister(id: number): void;
|
||||
}
|
||||
|
||||
const dispatchers = new Map<number, Dispatcher>();
|
||||
const scheduler: Scheduler = {
|
||||
now: () => performance.now(),
|
||||
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
||||
clearTimeout: handle => clearTimeout(handle as ReturnType<typeof setTimeout>)
|
||||
};
|
||||
|
||||
const logger = new Logger("WaylandKeybinds");
|
||||
const KeybindsStore = findStoreLazy("KeybindsStore");
|
||||
|
||||
function discordUtils() {
|
||||
return DiscordNative.nativeModules.requireModule("discord_utils");
|
||||
}
|
||||
|
||||
function portalTrigger(shortcut: Shortcut): string {
|
||||
return discordUtils().getWaylandPortalTrigger?.(shortcut.map(([type, code]) => [type, code])) ?? "";
|
||||
}
|
||||
|
||||
export default definePlugin({
|
||||
name: "WaylandKeybinds",
|
||||
description: "Makes Discord's global keybinds reliable on GNOME Wayland: toggles (mute, deafen) fire on key press and push-to-talk recovers from key releases GNOME drops",
|
||||
authors: [{ name: "Evandro", id: 281463271758888960n }],
|
||||
// only changes behaviour when Discord registers keybinds through the Wayland GlobalShortcuts portal
|
||||
enabledByDefault: true,
|
||||
patches: PATCHES,
|
||||
|
||||
wrapRegistration(bridge: Bridge, id: number, shortcut: Shortcut, callback: Callback, options: KeyEventOptions): [Callback, KeyEventOptions] {
|
||||
try {
|
||||
const portalActive = IS_LINUX && discordUtils().isWaylandGlobalShortcutsAvailable?.() === true;
|
||||
const mode = planRegistration(options, portalActive, portalActive && portalTrigger(shortcut) !== "");
|
||||
const adjusted = adjustOptions(options, mode);
|
||||
dispatchers.get(id)?.cancel();
|
||||
const dispatcher = createDispatcher(callback, mode, () => {
|
||||
bridge.inputEventUnregister(id);
|
||||
bridge.inputEventRegister(id, shortcut, dispatcher.dispatch, adjusted);
|
||||
}, scheduler);
|
||||
dispatchers.set(id, dispatcher);
|
||||
return [dispatcher.dispatch, adjusted];
|
||||
} catch (e) {
|
||||
logger.error("Failed to adapt keybind registration", e);
|
||||
return [callback, options];
|
||||
}
|
||||
},
|
||||
|
||||
unregister(bridge: Bridge, id: number) {
|
||||
// stop probing first, or the next re-registration would bring a deleted keybind back
|
||||
dispatchers.get(id)?.cancel();
|
||||
dispatchers.delete(id);
|
||||
bridge.inputEventUnregister(id);
|
||||
},
|
||||
|
||||
fixPortalState<S extends PortalState & { eventId: number; }>(state: S): S {
|
||||
try {
|
||||
const shortcut: Shortcut | undefined = KeybindsStore.getUserAgnosticState()?.[state.eventId]?.shortcut;
|
||||
return fixPortalState(state, shortcut?.length ? portalTrigger(shortcut) : undefined);
|
||||
} catch (e) {
|
||||
logger.error("Failed to check portal shortcut state", e);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user