diff --git a/package.json b/package.json index 3a31179a..41ceccfb 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ }, "dependencies": { "arrpc": "github:OpenAsar/arrpc#2234e9c9111f4c42ebcc3aa6a2215bfd979eef77", - "electron-updater": "^6.3.9" + "electron-updater": "^6.3.9", + "venbind": "^0.0.3" }, "optionalDependencies": { "@vencord/venmic": "^6.1.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a97c567e..8a3034ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ importers: electron-updater: specifier: ^6.3.9 version: 6.3.9 + venbind: + specifier: ^0.0.3 + version: 0.0.3 optionalDependencies: '@vencord/venmic': specifier: ^6.1.0 @@ -2877,6 +2880,11 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + venbind@0.0.3: + resolution: {integrity: sha512-irlmr5Qeo9pSejGF5nATwzn5ramkEuJzjDMExrPA0p1DrNfFpUqyDsA05edUfHB2R4S9iyuILZG9zDQVKB3O3w==} + cpu: [x64] + os: [linux, win32] + verror@1.10.1: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} @@ -6159,6 +6167,8 @@ snapshots: util-deprecate@1.0.2: {} + venbind@0.0.3: {} + verror@1.10.1: dependencies: assert-plus: 1.0.0 diff --git a/scripts/build/build.mts b/scripts/build/build.mts index b4455177..ffea7e9e 100644 --- a/scripts/build/build.mts +++ b/scripts/build/build.mts @@ -49,8 +49,27 @@ async function copyVenmic() { ]).catch(() => console.warn("Failed to copy venmic. Building without venmic support")); } +async function copyVenbind() { + if (process.platform === "win32") { + return Promise.all([ + copyFile( + "./node_modules/venbind/prebuilds/windows-x86_64/venbind-windows-x86_64.node", + "./static/dist/venbind-windows-x86_64.node" + ) + ]).catch(() => console.warn("Failed to copy venbind. Building without venbind support")); + } + + return Promise.all([ + copyFile( + "./node_modules/venbind/prebuilds/linux-x86_64/venbind-linux-x86_64.node", + "./static/dist/venbind-linux-x86_64.node" + ) + ]).catch(() => console.warn("Failed to copy venbind. Building without venbind support")); +} + await Promise.all([ copyVenmic(), + copyVenbind(), createContext({ ...NodeCommonOpts, entryPoints: ["src/main/index.ts"], diff --git a/src/main/index.ts b/src/main/index.ts index f1bc6170..b05ff97f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -16,6 +16,7 @@ import { registerMediaPermissionsHandler } from "./mediaPermissions"; import { registerScreenShareHandler } from "./screenShare"; import { Settings, State } from "./settings"; import { isDeckGameMode } from "./utils/steamOS"; +import { startVenbind } from "./venbind"; if (IS_DEV) { require("source-map-support").install(); @@ -78,8 +79,18 @@ function init() { // In the Flatpak on SteamOS the theme is detected as light, but SteamOS only has a dark mode, so we just override it if (isDeckGameMode) nativeTheme.themeSource = "dark"; - app.on("second-instance", (_event, _cmdLine, _cwd, data: any) => { - if (data.IS_DEV) app.quit(); + app.on("second-instance", (_event, cmdLine, _cwd, data: any) => { + const keybindIndex = cmdLine.indexOf("--keybind"); + + if (keybindIndex !== -1) { + if (cmdLine[keybindIndex + 2] === "keyup" || cmdLine[keybindIndex + 2] === "keydown") { + mainWin.webContents.executeJavaScript( + `Vesktop.triggerKeybind(${cmdLine[keybindIndex + 1]}, ${cmdLine[keybindIndex + 2] === "keydown" ? "false" : "true"})` + ); + } else { + mainWin.webContents.executeJavaScript(`Vesktop.triggerKeybind(${cmdLine[keybindIndex + 1]}, true)`); + } + } else if (data.IS_DEV) app.quit(); else if (mainWin) { if (mainWin.isMinimized()) mainWin.restore(); if (!mainWin.isVisible()) mainWin.show(); @@ -90,6 +101,7 @@ function init() { app.whenReady().then(async () => { if (process.platform === "win32") app.setAppUserModelId("dev.vencord.vesktop"); + startVenbind(); registerScreenShareHandler(); registerMediaPermissionsHandler(); @@ -102,15 +114,24 @@ function init() { } if (!app.requestSingleInstanceLock({ IS_DEV })) { - if (IS_DEV) { - console.log("Vesktop is already running. Quitting previous instance..."); - init(); - } else { - console.log("Vesktop is already running. Quitting..."); + if (process.argv.includes("--keybind")) { app.quit(); + } else { + if (IS_DEV) { + console.log("Vesktop is already running. Quitting previous instance..."); + init(); + } else { + console.log("Vesktop is already running. Quitting..."); + app.quit(); + } } } else { - init(); + if (process.argv.includes("--keybind")) { + console.error("No instances running! cannot issue a keybind!"); + app.quit(); + } else { + init(); + } } async function bootstrap() { diff --git a/src/main/venbind.ts b/src/main/venbind.ts new file mode 100644 index 00000000..9a456275 --- /dev/null +++ b/src/main/venbind.ts @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { join } from "path"; +import { IpcEvents } from "shared/IpcEvents"; +import { STATIC_DIR } from "shared/paths"; +import type { Venbind as VenbindType } from "venbind"; + +import { mainWin } from "./mainWindow"; +import { handle, handleSync } from "./utils/ipcWrappers"; + +let venbind: VenbindType | null = null; +export function obtainVenbind() { + if (venbind == null) { + // TODO?: make binary outputs consistant with node's apis + let os: string; + let arch: string; + + switch (process.platform) { + case "linux": + os = "linux"; + break; + case "win32": + os = "windows"; + break; + // case "darwin": + // os = "darwin"; + // break; + default: + return null; + } + switch (process.arch) { + case "x64": + arch = "x86_64"; + break; + // case "arm64": + // arch = "aarch64"; + // break; + default: + return null; + } + + venbind = require(join(STATIC_DIR, `dist/venbind-${os}-${arch}.node`)); + } + return venbind; +} + +export function startVenbind() { + const venbind = obtainVenbind(); + venbind?.startKeybinds((id, keyup) => { + mainWin.webContents.executeJavaScript(`Vesktop.triggerKeybind(${id}, ${keyup})`); + }); +} + +handle(IpcEvents.KEYBIND_REGISTER, (_, id: number, shortcut: string) => { + obtainVenbind()?.registerKeybind(shortcut, id); +}); +handle(IpcEvents.KEYBIND_UNREGISTER, (_, id: number) => { + obtainVenbind()?.unregisterKeybind(id); +}); +handleSync(IpcEvents.KEYBIND_SHOULD_PREREGISTER, _ => { + if ( + process.platform === "linux" && + (process.env.XDG_SESSION_TYPE === "wayland" || + !!process.env.WAYLAND_DISPLAY || + !!process.env.VENBIND_USE_XDG_PORTAL) + ) { + return true; + } + return false; +}); +handle(IpcEvents.KEYBIND_PREREGISTER, (_, actions: { id: number; name: string }[]) => { + obtainVenbind()?.preregisterKeybinds(actions); +}); diff --git a/src/preload/VesktopNative.ts b/src/preload/VesktopNative.ts index 69884cd7..58c69816 100644 --- a/src/preload/VesktopNative.ts +++ b/src/preload/VesktopNative.ts @@ -84,5 +84,11 @@ export const VesktopNative = { ipcRenderer.on(IpcEvents.IPC_COMMAND, (_, message) => cb(message)); }, respond: (response: IpcResponse) => ipcRenderer.send(IpcEvents.IPC_COMMAND, response) + }, + keybind: { + register: (id: number, shortcut: string) => invoke(IpcEvents.KEYBIND_REGISTER, id, shortcut), + unregister: (id: number) => invoke(IpcEvents.KEYBIND_UNREGISTER, id), + shouldPreRegister: () => sendSync(IpcEvents.KEYBIND_SHOULD_PREREGISTER), + preRegister: (actions: { id: number; name: string }[]) => invoke(IpcEvents.KEYBIND_PREREGISTER, actions) } }; diff --git a/src/renderer/index.ts b/src/renderer/index.ts index 0f863cc2..ca8aa82e 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -18,9 +18,28 @@ import { VesktopLogger } from "./logger"; import { Settings } from "./settings"; export { Settings }; +export const keybindCallbacks: { + [id: number]: { + onTrigger: Function; + keyEvents: { + keyup: boolean; + keydown: boolean; + }; + }; +} = {}; + VesktopLogger.log("read if cute :3"); VesktopLogger.log("Vesktop v" + VesktopNative.app.getVersion()); +export async function triggerKeybind(id: number, keyup: boolean) { + var cb = keybindCallbacks[id]; + if (cb.keyEvents.keyup && keyup) { + cb.onTrigger(false); + } else if (cb.keyEvents.keydown && !keyup) { + cb.onTrigger(true); + } +} + const customSettingsSections = ( Vencord.Plugins.plugins.Settings as any as { customSections: ((ID: Record) => any)[] } ).customSections; diff --git a/src/renderer/patches/index.ts b/src/renderer/patches/index.ts index 439d5582..41a53b42 100644 --- a/src/renderer/patches/index.ts +++ b/src/renderer/patches/index.ts @@ -15,3 +15,4 @@ import "./windowsTitleBar"; import "./streamerMode"; import "./nativeFocus"; import "./hideDownloadAppsButton"; +import "./keybinds"; diff --git a/src/renderer/patches/keybinds.tsx b/src/renderer/patches/keybinds.tsx new file mode 100644 index 00000000..491274ee --- /dev/null +++ b/src/renderer/patches/keybinds.tsx @@ -0,0 +1,171 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { findByCodeLazy } from "@vencord/types/webpack"; +import { keybindCallbacks } from "renderer"; + +import { addPatch } from "./shared"; +import { ErrorCard } from "@vencord/types/components"; +const toShortcutString = findByCodeLazy('.MOUSE_BUTTON?"mouse".concat('); +const actionReadableNames: { [key: string]: string } = { + PUSH_TO_TALK: "Push To Talk", + PUSH_TO_TALK_PRIORITY: "Push To Talk (Priority)", + PUSH_TO_MUTE: "Push To Mute", + TOGGLE_MUTE: "Toggle Mute", + TOGGLE_DEAFEN: "Toggle Deafen", + TOGGLE_VOICE_MODE: "Toggle Voice Activity Mode", + TOGGLE_STREAMER_MODE: "Toggle Streamer Mode", + NAVIGATE_BACK: "Navigate Back", + NAVIGATE_FORWARD: "Navigate Forward", + DISCONNECT_FROM_VOICE_CHANNEL: "Disconnect From Voice Channel" +}; +const actions: { id: number; name: string }[] = []; +addPatch({ + patches: [ + { + find: "keybindActionTypes", + replacement: [ + { + // eslint-disable-next-line no-useless-escape + match: /(\i\.isPlatformEmbedded\?)(.+renderEmpty\(\i\)\]\}\)\]\}\))/, + replace: "$1$self.xdpWarning($2)" + }, + { + // eslint-disable-next-line no-useless-escape + match: /\i\.isPlatformEmbedded/g, + replace: "true" + }, + { + // eslint-disable-next-line no-useless-escape + match: /\(0,\i\.isDesktop\)\(\)/g, + replace: "true" + }, + { + // THIS PATCH IS TEMPORARY + // eslint-disable-next-line no-useless-escape + match: /\.keybindGroup,\i.card\),children:\[/g, + replace: "$&`ID: ${this.props.keybind.id}`," + } + ] + }, + { + find: "[kb store] KeybindStore", + replacement: [ + { + // eslint-disable-next-line no-useless-escape + match: /inputEventRegister\((parseInt\(\i\),\i,\i,\i)\);else\{/, + replace: "$&$self.registerKeybind($1);return;" + }, + { + // eslint-disable-next-line no-useless-escape + match: /inputEventUnregister\((parseInt\(\i,10\))\);else/, + replace: "$&{$self.unregisterKeybind($1);return;}" + }, + { + // eslint-disable-next-line no-useless-escape + match: /let{keybinds:(\i)}=\i;/, + replace: "$&$self.preRegisterKeybinds($1);" + } + ] + } + ], + + registerKeybind: function ( + id, + shortcut, + callback: Function, + options: { + keyup: boolean; + keydown: boolean; + } + ) { + if (VesktopNative.keybind.shouldPreRegister()) { + return; + } + keybindCallbacks[id] = { + onTrigger: callback, + keyEvents: options + }; + VesktopNative.keybind.register(id, toShortcutString(shortcut)); + }, + unregisterKeybind: function (id) { + if (VesktopNative.keybind.shouldPreRegister()) { + return; + } + delete keybindCallbacks[id]; + VesktopNative.keybind.unregister(id); + }, + // only used for wayland/xdg-desktop-portal globalshortcuts + preRegisterKeybinds: function (allActions: { + [action: string]: { + onTrigger: Function; + keyEvents: { + keyup: boolean; + keydown: boolean; + }; + }; + }) { + if (!VesktopNative.keybind.shouldPreRegister()) { + return; + } + let id = 1; + Object.entries(allActions).forEach(([key, val]) => { + if ( + [ + "UNASSIGNED", + "SWITCH_TO_VOICE_CHANNEL", + "TOGGLE_OVERLAY", + "TOGGLE_OVERLAY_INPUT_LOCK", + "TOGGLE_PRIORITY_SPEAKER", + "OVERLAY_ACTIVATE_REGION_TEXT_WIDGET", + "TOGGLE_GO_LIVE_STREAMING", // ??? + "SOUNDBOARD", + "SOUNDBOARD_HOLD", + "SAVE_CLIP" + // most of these aren't available to change through discord as far as i can tell + ].includes(key) + ) { + return; + } + keybindCallbacks[id] = { + onTrigger: (keyState: boolean) => + val.onTrigger(keyState, { + // switch to channel also requires some extra properties that would have to be supplied here + context: undefined + }), + keyEvents: val.keyEvents + }; + actions.push({ id, name: actionReadableNames[key] || key }); + id++; + }); + }, + xdpWarning: function (keybinds) { + if (!VesktopNative.keybind.shouldPreRegister()) { + return keybinds; + } + VesktopNative.keybind.preRegister(actions); + return ( + +

+ You appear to be using Vesktop on a platform that requires XDG desktop portals for using keybinds. + You can configure keybinds using your desktop environment's built-in settings page. +

+

+ If your desktop environment does not support the GlobalShortcuts portal (which you would know if its + settings page didn't open just now) you have to manually bind your desired keybinds to CLI triggers. +

+

List of valid keybind IDs to use with the CLI:

+
    + {actions.map(keybind => ( +
  • + {keybind.id}: {keybind.name} +
  • + ))} +
+
+ ); + } +}); diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index dd05e3ba..e33040ac 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -55,7 +55,12 @@ export const enum IpcEvents { DEBUG_LAUNCH_GPU = "VCD_DEBUG_LAUNCH_GPU", DEBUG_LAUNCH_WEBRTC_INTERNALS = "VCD_DEBUG_LAUNCH_WEBRTC", - IPC_COMMAND = "VCD_IPC_COMMAND" + IPC_COMMAND = "VCD_IPC_COMMAND", + + KEYBIND_REGISTER = "VCD_KEYBIND_REGISTER", + KEYBIND_UNREGISTER = "VCD_KEYBIND_UNREGISTER", + KEYBIND_SHOULD_PREREGISTER = "VCD_KEYBIND_SHOULD_PREREGISTER", + KEYBIND_PREREGISTER = "VCD_KEYBIND_PREREGISTER" } export const enum IpcCommands {