Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ac21b1d
Added keybind setting type for global shortcuts, and api to register …
nicola02nb Aug 18, 2025
5ef72dc
Addded plugin example, porting of ShortcutScreenshareScreen from BD
nicola02nb Aug 18, 2025
a3e6223
Plugin cleanup
nicola02nb Aug 22, 2025
165df60
Added surpport global setting to make the component return EventKey.k…
nicola02nb Aug 25, 2025
2e427b8
Add global setting for keybinds in shortcut settings
nicola02nb Aug 25, 2025
2d4883b
Merge branch 'main' into global-shortcuts-setting
nicola02nb Aug 25, 2025
e85ad9b
Added also registering for window keybinds
nicola02nb Aug 26, 2025
13b5656
Added mouse keys and gamepad keys for window shortcut
nicola02nb Aug 26, 2025
ea501c8
Removed some console.log
nicola02nb Aug 26, 2025
512955e
Commended out controller part not working atm, added tooltip keybind
nicola02nb Aug 26, 2025
77bc434
global now cannoti be undefined, added switch to enable disable the k…
nicola02nb Aug 26, 2025
21f9f31
Improve tooltip text for keybind settings and enhance clarity in UI m…
nicola02nb Aug 26, 2025
95b0771
added icon to distinguish window form global keybind in settings comp…
nicola02nb Aug 26, 2025
c9aeb69
Done custom component for keybinds, missing window keys registration
nicola02nb Aug 27, 2025
75c092c
Some things
nicola02nb Aug 28, 2025
191225f
Merge branch 'main' into global-shortcuts-setting
nicola02nb Aug 28, 2025
914110d
Some refactor
nicola02nb Aug 29, 2025
a648921
Classes update
nicola02nb Aug 30, 2025
55f5af3
Close to an end
nicola02nb Sep 1, 2025
6534cf6
Fix inputCaptureKeysWindow
nicola02nb Sep 1, 2025
d007707
Now shoul add done except some TODO for adding controller into window…
nicola02nb Sep 1, 2025
1f24306
All done except missing TODOs for window gamepad and checks if not De…
nicola02nb Sep 1, 2025
fc58516
Added a check for discord app
nicola02nb Sep 1, 2025
34ea03a
Merge branch 'main' into global-shortcuts-setting
nicola02nb Sep 2, 2025
cdcfef6
Merge branch 'main' into global-shortcuts-setting
nicola02nb Sep 5, 2025
ce509bc
Merge branch 'main' into global-shortcuts-setting
nicola02nb Sep 9, 2025
c724466
Merge branch 'main' into global-shortcuts-setting
nicola02nb Sep 25, 2025
9c448d2
Merge branch 'main' into global-shortcuts-setting
nicola02nb Oct 14, 2025
eadab2a
Merge branch 'main' into global-shortcuts-setting
nicola02nb Oct 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/discord-types/src/components.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ComponentClass, ComponentPropsWithRef, ComponentType, CSSProperties, FunctionComponent, HtmlHTMLAttributes, HTMLProps, JSX, KeyboardEvent, MouseEvent, PointerEvent, PropsWithChildren, ReactNode, Ref, RefObject } from "react";
import { GlobalShortcut } from "./utils";


// #region Old compability
Expand Down Expand Up @@ -513,3 +514,9 @@ export type ColorPicker = ComponentType<{
label?: ReactNode;
onChange(value: number | null): void;
}>;

export type GlobalKeybind = ComponentType<{
defaultValue: GlobalShortcut;
disabled: boolean;
onChange(value: GlobalShortcut): void;
}>;
30 changes: 30 additions & 0 deletions packages/discord-types/src/utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,33 @@ export interface CommandOptions {
max_value?: number;
autocomplete?: boolean;
}

// Global shortcut types
export enum GlobalShortcutKeyOS {
WINDOWS = 1,
MACOS = 2,
LINUX = 3,
BROWSER = 4
}
export enum GlobalShortcutKeyType {
KEYBOARD_KEY = 0,
MOUSE_BUTTON = 1,
KEYBOARD_MODIFIER_KEY = 2,
GAMEPAD_BUTTON = 3
}
export type GlobalShortcutKey = [GlobalShortcutKeyType, number] | [GlobalShortcutKeyType, number, GlobalShortcutKeyOS | `${number}:${number}`];
export type GlobalShortcut = GlobalShortcutKey[];
export type GlobalShortcutOptions = {
blurred: boolean;
focused: boolean;
keydown: boolean;
keyup: boolean;
};

export type DiscordUtils = {
inputCaptureRegisterElement(elementId: string, callback: (keys: GlobalShortcut) => void): () => void;
inputGetRegisteredEvents(callback: (keys: GlobalShortcut) => void): undefined;
inputEventRegister(id: number, keys: GlobalShortcut, callback: () => void, options: GlobalShortcutOptions): undefined;
inputEventUnregister(id: number): undefined;
inputSetFocused(focused: boolean): undefined;
};
79 changes: 79 additions & 0 deletions src/api/Keybinds/globalManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { DiscordUtils, GlobalShortcut, GlobalShortcutOptions } from "@vencord/discord-types";
import { findByCodeLazy } from "webpack";

import { KeybindManager } from "./types";

// Discord mapping from keycodes array to string (mouse, keyboard, gamepad)
const keycodesToString = IS_DISCORD_DESKTOP ? findByCodeLazy(".map(", ".KEYBOARD_KEY", ".KEYBOARD_MODIFIER_KEY", ".MOUSE_BUTTON", ".GAMEPAD_BUTTON") as (keys: GlobalShortcut) => string : (keys: GlobalShortcut) => keys.join("+");

export default new class GlobalManager implements KeybindManager {
private discordUtils: undefined | DiscordUtils; // TODO: Maybe check if IS_VESKTOP and use its global keybinds api
private lastGlobalId: number = 1000;
private mapIdToEvent: Map<string, number> = new Map();

private initDiscordUtils() {
if (!IS_DISCORD_DESKTOP || this.discordUtils || !DiscordNative) return;
this.discordUtils = DiscordNative.nativeModules.requireModule("discord_utils");
}

// From discord key registration
private newKeysInstance(keys: GlobalShortcut): GlobalShortcut {
return keys.map(e => {
const [t, n, r] = e;
return typeof r === "string" ? [t, n, r] : [t, n];
});
}

private getIdForEvent(event: string): number {
const found = this.mapIdToEvent.get(event);
if (!found) {
const id = this.lastGlobalId++;
this.mapIdToEvent.set(event, id);
return id;
} else {
return found;
}
}

public isAvailable() {
this.initDiscordUtils();
return !!this.discordUtils;
}

public getDiscordUtils() {
this.initDiscordUtils();
return this.discordUtils;
}

public registerKeybind(event: string, keys: GlobalShortcut, callback: () => void, options: GlobalShortcutOptions) {
this.initDiscordUtils();
if (!this.discordUtils) return;
const id = this.getIdForEvent(event);
if (!id) return;
this.discordUtils.inputEventRegister(id, this.newKeysInstance(keys), callback, options);
}

public unregisterKeybind(event: string) {
this.initDiscordUtils();
if (!this.discordUtils) return;
const id = this.mapIdToEvent.get(event);
if (!id) return;
this.discordUtils.inputEventUnregister(id);
}

public inputCaptureKeys(inputId: string, callback: (keys: GlobalShortcut) => void): () => void {
this.initDiscordUtils();
if (!this.discordUtils) return () => { };
return this.discordUtils.inputCaptureRegisterElement(inputId, callback);
}

public keysToString(keys: GlobalShortcut): string {
return keycodesToString(keys).toUpperCase();
}
};
9 changes: 9 additions & 0 deletions src/api/Keybinds/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import keybindsManager from "./keybindsManager";

export { keybindsManager };
94 changes: 94 additions & 0 deletions src/api/Keybinds/keybindsManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { GlobalShortcut, GlobalShortcutOptions } from "@vencord/discord-types";

import globalManager from "./globalManager";
import { InternalKeybind, Keybind, KeybindShortcut, WindowShortcut } from "./types";
import windowManager from "./windowManager";

export default new class KeybindsManager {
private keybindsGlobal: Map<string, InternalKeybind> = new Map();
private keybindsWindow: Map<string, InternalKeybind> = new Map();

isAvailable(global: boolean) {
return global ? globalManager.isAvailable() : windowManager.isAvailable();
}

inputCaptureKeys(inputId: string, callback: (keys: KeybindShortcut) => void, global: boolean) {
return global ? globalManager.inputCaptureKeys(inputId, callback) : windowManager.inputCaptureKeys(inputId, callback);
}

keysToString(keys: KeybindShortcut, global: boolean): string {
return global ? globalManager.keysToString(keys as GlobalShortcut) : windowManager.keysToString(keys as WindowShortcut);
}

private getBinding(event: string, global: boolean): InternalKeybind | undefined {
return global ? this.keybindsGlobal.get(event) : this.keybindsWindow.get(event);
}

private isEventAvailable(event: string, global: boolean): boolean {
return global ? !this.keybindsGlobal.has(event) : !this.keybindsWindow.has(event);
}

registerKeybind(binding: Keybind, keys: KeybindShortcut = []) {
if (!this.isEventAvailable(binding.event, binding.global)) return false;
if (binding.global) {
this.keybindsGlobal.set(binding.event, { keys: (keys as GlobalShortcut), enabled: false, ...(binding as Keybind) });
} else {
this.keybindsWindow.set(binding.event, { keys: (keys as WindowShortcut), enabled: false, ...(binding as Keybind) });
}
return true;
}

unregisterKeybind(event: string, global: boolean): boolean {
const binding = this.getBinding(event, global);
if (!binding) return false;
if (binding.enabled) {
this.disableKeybind(event, global);
}
return global ? this.keybindsGlobal.delete(event) : this.keybindsWindow.delete(event);
}

updateKeybind(event: string, keys: KeybindShortcut, global: boolean) {
const binding = this.getBinding(event, global);
if (!binding) return;
binding.keys = keys;
if (binding.enabled) {
this.disableKeybind(event, global);
}
this.enableKeybind(event, global);
}

enableKeybind(event: string, global: boolean) {
const binding = this.getBinding(event, global);
if (!binding) return;
if (binding.enabled || !binding.keys.length) return;
if (global) {
globalManager.registerKeybind(binding.event, binding.keys as GlobalShortcut, binding.function, binding.options as GlobalShortcutOptions);
} else {
windowManager.registerKeybind(binding.event, binding.keys as WindowShortcut, binding.function, binding.options);
}
binding.enabled = true;
}

disableKeybind(event: string, global: boolean) {
if (global) {
if (!globalManager.isAvailable()) return;
const binding = this.getBinding(event, true);
if (!binding) return;
if (!binding.enabled) return;
globalManager.unregisterKeybind(binding.event);
binding.enabled = false;
} else {
const binding = this.getBinding(event, false);
if (!binding) return;
if (!binding.enabled) return;
windowManager.unregisterKeybind(binding.event);
binding.enabled = false;
}
}
};
36 changes: 36 additions & 0 deletions src/api/Keybinds/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/

import { GlobalShortcut, GlobalShortcutOptions } from "@vencord/discord-types";

export type WindowShortcut = string[];
export type WindowShortcutOptions = {
keydown: boolean;
keyup: boolean;
};

export type KeybindShortcut = GlobalShortcut | WindowShortcut;
export type KeybindOptions = GlobalShortcutOptions | WindowShortcutOptions;

export type Keybind = {
event: string;
function: () => void;
options: KeybindOptions;
global: boolean;
};

export type InternalKeybind = Keybind & {
enabled: boolean;
keys: KeybindShortcut;
};

interface KeybindManager {
isAvailable(): boolean;
registerKeybind(event: string, keys: KeybindShortcut, callback: () => void, options: GlobalShortcutOptions): void;
unregisterKeybind(event: string): void;
inputCaptureKeys(inputId: string, callback: (keys: KeybindShortcut) => void): () => void;
keysToString(keys: KeybindShortcut): string;
}
Loading