Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 71 additions & 0 deletions companion-bridge/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* eslint-disable no-console */

const { GlobalKeyboardListener } = require("node-global-key-listener");
const { WebSocketServer } = require("ws");

const PORT = 6969;
const pressedKeys = new Set();

const wss = new WebSocketServer({ host: "127.0.0.1", port: PORT });
const keyboard = new GlobalKeyboardListener();

function broadcast(action) {
const payload = JSON.stringify({ action });

for (const client of wss.clients) {
if (client.readyState === client.OPEN) {
client.send(payload);
}
}
}

function handleKeyDown(name) {
if (pressedKeys.has(name)) return;

pressedKeys.add(name);

if (name === "INSERT") {
broadcast("TOGGLE_MUTE");
} else if (name === "HOME") {
broadcast("TOGGLE_DEAFEN");
}
}

function handleKeyUp(name) {
pressedKeys.delete(name);
}

keyboard.addListener(event => {
if (event.state === "DOWN") {
handleKeyDown(event.name);
return;
}

if (event.state === "UP") {
handleKeyUp(event.name);
}
});

wss.on("listening", () => {
console.log(`Companion bridge listening on ws://localhost:${PORT}`);
});

wss.on("connection", socket => {
socket.on("error", error => {
console.error("Companion bridge socket error:", error);
});
});

wss.on("error", error => {
console.error("Companion bridge server error:", error);
});

function shutdown() {
keyboard.kill();
wss.close(() => {
process.exit(0);
});
}

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
15 changes: 15 additions & 0 deletions companion-bridge/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "companion-bridge",
"private": true,
"version": "1.0.0",
"description": "Sidecar bridge for Vencord global mute and deafen keybinds",
"main": "index.js",
"license": "GPL-3.0-or-later",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"node-global-key-listener": "^0.3.0",
"ws": "^8.18.3"
}
}
99 changes: 39 additions & 60 deletions src/userplugins/customkeybinds/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,18 @@
*/

import { Logger } from "@utils/Logger";
import definePlugin, { PluginNative } from "@utils/types";
import definePlugin from "@utils/types";
import { findByProps } from "@webpack";
import { FluxDispatcher, showToast, Toasts } from "@webpack/common";

import type * as NativeModule from "./native";

type GlobalToggleAction = "mute" | "deafen";
type BridgeAction = "TOGGLE_MUTE" | "TOGGLE_DEAFEN";
type SoundPlayer = { playSound: (sound: string, volume?: number) => void; };
type BridgeMessage = { action?: unknown; };

const logger = new Logger("CustomKeybinds");
const Native = IS_DISCORD_DESKTOP || IS_VESKTOP
? VencordNative.pluginHelpers.CustomKeybinds as PluginNative<typeof NativeModule>
: null;

const SoundModule = findByProps("playSound") as Partial<SoundPlayer> | undefined;
const BridgeUrl = "ws://localhost:6969";

const ToggleEventName = "vc-custom-keybinds-global-toggle";
const ErrorEventName = "vc-custom-keybinds-global-error";

function getDesktopBridge(): PluginNative<typeof NativeModule> {
if (!Native) {
throw new Error("CustomKeybinds requires the desktop native bridge.");
}

return Native;
}
let bridgeSocket: WebSocket | null = null;

function getSoundPlayer(): SoundPlayer {
const playSound = SoundModule?.playSound;
Expand All @@ -42,81 +28,74 @@ function getSoundPlayer(): SoundPlayer {
return { playSound };
}

function handleToggle(action: GlobalToggleAction) {
function handleToggle(action: BridgeAction) {
const soundPlayer = getSoundPlayer();

FluxDispatcher.dispatch({
type: action === "mute"
type: action === "TOGGLE_MUTE"
? "AUDIO_TOGGLE_SELF_MUTE"
: "AUDIO_TOGGLE_SELF_DEAF"
});

soundPlayer.playSound(action, 0.5);
soundPlayer.playSound(action === "TOGGLE_MUTE" ? "mute" : "deafen", 0.5);
}

function onGlobalToggle(event: Event) {
const action = (event as CustomEvent<unknown>).detail;

if (action !== "mute" && action !== "deafen") {
const message = `Received invalid global toggle action: ${String(action)}`;
logger.error(message);
showToast(message, Toasts.Type.FAILURE);
function onBridgeMessage(event: MessageEvent) {
if (typeof event.data !== "string") {
logger.error("Received non-text message from companion bridge:", event.data);
return;
}

handleToggle(action);
}
let payload: BridgeMessage;

function onNativeError(event: Event) {
const message = (event as CustomEvent<unknown>).detail;
try {
payload = JSON.parse(event.data) as BridgeMessage;
} catch (error) {
logger.error("Failed to parse companion bridge message:", error);
showToast("CustomKeybinds received invalid JSON from the companion bridge.", Toasts.Type.FAILURE);
return;
}

if (typeof message !== "string" || !message.length) {
logger.error("Received invalid native bridge error payload:", message);
showToast("CustomKeybinds native bridge failed with an invalid error payload.", Toasts.Type.FAILURE);
if (payload.action !== "TOGGLE_MUTE" && payload.action !== "TOGGLE_DEAFEN") {
const message = `Received invalid companion bridge action: ${String(payload.action)}`;
logger.error(message);
showToast(message, Toasts.Type.FAILURE);
return;
}

logger.error(message);
showToast(message, Toasts.Type.FAILURE);
handleToggle(payload.action);
}

export default definePlugin({
name: "CustomKeybinds",
description: "Registers true global mute and deafen toggles for Vesktop/Discord desktop",
description: "Receives true global mute and deafen toggles from the companion bridge",
authors: [{ name: "Jules", id: 0n }],
start() {
let native: PluginNative<typeof NativeModule>;

try {
native = getDesktopBridge();
getSoundPlayer();
} catch (error) {
logger.error("Failed to initialize CustomKeybinds:", error);
showToast(error instanceof Error ? error.message : "Failed to initialize CustomKeybinds.", Toasts.Type.FAILURE);
return;
}

window.addEventListener(ToggleEventName, onGlobalToggle as EventListener);
window.addEventListener(ErrorEventName, onNativeError as EventListener);
bridgeSocket?.close();

void native.start().catch(error => {
const message = error instanceof Error ? error.message : "Failed to start native global toggle bridge.";
logger.error("Failed to start native global toggle bridge:", error);
showToast(message, Toasts.Type.FAILURE);
bridgeSocket = new WebSocket(BridgeUrl);
bridgeSocket.addEventListener("message", onBridgeMessage);
bridgeSocket.addEventListener("error", error => {
logger.error("Companion bridge WebSocket error:", error);
});
},
stop() {
window.removeEventListener(ToggleEventName, onGlobalToggle as EventListener);
window.removeEventListener(ErrorEventName, onNativeError as EventListener);

if (!Native) return;
bridgeSocket.addEventListener("close", event => {
if (event.wasClean) return;

void Native.stop().catch(error => {
logger.error("Failed to stop native global toggle bridge:", error);
showToast(
error instanceof Error ? error.message : "Failed to stop native global toggle bridge.",
Toasts.Type.FAILURE
);
logger.error(`Companion bridge connection closed unexpectedly (code: ${event.code}).`);
showToast("CustomKeybinds lost its connection to the companion bridge.", Toasts.Type.FAILURE);
});
},
stop() {
bridgeSocket?.removeEventListener("message", onBridgeMessage);
bridgeSocket?.close();
bridgeSocket = null;
}
});
Loading
Loading