├── packages └── libvesktop │ ├── .gitignore │ ├── prebuilds │ ├── vesktop-x64.node │ └── vesktop-arm64.node │ ├── index.d.ts │ ├── package.json │ ├── build.sh │ ├── binding.gyp │ ├── Dockerfile │ └── test.js ├── static ├── dist │ └── .gitignore ├── tray.png ├── splash.webp ├── badges │ ├── 1.ico │ ├── 10.ico │ ├── 11.ico │ ├── 2.ico │ ├── 3.ico │ ├── 4.ico │ ├── 5.ico │ ├── 6.ico │ ├── 7.ico │ ├── 8.ico │ └── 9.ico ├── tray │ ├── tray.png │ ├── trayUnread.png │ └── trayTemplate.png └── views │ ├── common.css │ ├── splash.html │ ├── updater │ ├── index.html │ ├── style.css │ └── script.js │ ├── about.html │ └── first-launch.html ├── .npmrc ├── .gitignore ├── .prettierrc.yaml ├── scripts ├── build │ ├── afterPack.mjs │ ├── beforePack.mjs │ ├── injectReact.mjs │ ├── addAssetsCar.mjs │ ├── includeDirPlugin.mts │ ├── vencordDep.mts │ ├── sandboxFix.mjs │ └── build.mts ├── header.txt ├── utils │ ├── dotenv.ts │ ├── spawn.mts │ └── generateMeta.mts ├── startWatch.mts └── start.ts ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── dev-issue.yml └── workflows │ ├── winget-submission.yml │ ├── test.yml │ ├── meta.yml │ ├── update-vencord-dev.yml │ └── release.yml ├── src ├── renderer │ ├── components │ │ ├── index.ts │ │ ├── settings │ │ │ ├── VesktopSettingsSwitch.tsx │ │ │ ├── UserAssets.css │ │ │ ├── settings.css │ │ │ ├── NotificationBadgeToggle.tsx │ │ │ ├── OutdatedVesktopWarning.tsx │ │ │ ├── DiscordBranchPicker.tsx │ │ │ ├── AutoStartToggle.tsx │ │ │ ├── WindowsTransparencyControls.tsx │ │ │ ├── UserAssets.tsx │ │ │ └── DeveloperOptions.tsx │ │ ├── SimpleErrorBoundary.tsx │ │ └── screenSharePicker.css │ ├── logger.ts │ ├── common.ts │ ├── patches │ │ ├── enableNotificationsByDefault.ts │ │ ├── hideDownloadAppsButton.ts │ │ ├── streamerMode.ts │ │ ├── shared.ts │ │ ├── hideSwitchDevice.tsx │ │ ├── hideVenmicInput.tsx │ │ ├── taskBarFlash.ts │ │ ├── platformClass.tsx │ │ ├── windowMethods.tsx │ │ ├── windowsTitleBar.tsx │ │ ├── devtoolsFixes.ts │ │ ├── fixAutoGainToggle.ts │ │ ├── screenShareFixes.ts │ │ └── spellCheck.tsx │ ├── utils.ts │ ├── fixes.ts │ ├── index.ts │ ├── ipcCommands.ts │ ├── appBadge.ts │ ├── settings.ts │ ├── arrpc.ts │ └── themedSplash.ts ├── module.d.ts ├── shared │ ├── utils │ │ ├── sleep.ts │ │ ├── millis.ts │ │ ├── guards.ts │ │ ├── text.ts │ │ ├── once.ts │ │ └── debounce.ts │ ├── paths.ts │ ├── browserWinProperties.ts │ ├── settings.d.ts │ └── IpcEvents.ts ├── main │ ├── utils │ │ ├── fileExists.ts │ │ ├── isPathInDirectory.ts │ │ ├── clearData.ts │ │ ├── setAsDefaultProtocolClient.ts │ │ ├── ipcWrappers.ts │ │ ├── desktopFileEscape.ts │ │ ├── http.ts │ │ ├── makeLinksOpenExternally.ts │ │ ├── vencordLoader.ts │ │ ├── popout.ts │ │ └── steamOS.ts │ ├── events.ts │ ├── vencordFilesDir.ts │ ├── about.ts │ ├── vesktopProtocol.ts │ ├── mediaPermissions.ts │ ├── arrpc │ │ ├── types.ts │ │ ├── worker.ts │ │ └── index.ts │ ├── vesktopStatic.ts │ ├── dbus.ts │ ├── settings.ts │ ├── splash.ts │ ├── ipcCommands.ts │ ├── appBadge.ts │ ├── constants.ts │ ├── tray.ts │ ├── firstLaunch.ts │ ├── screenShare.ts │ ├── updater.ts │ ├── userAssets.ts │ ├── autoStart.ts │ ├── venmic.ts │ └── cli.ts ├── globals.d.ts └── preload │ ├── splash.ts │ ├── typedIpc.ts │ ├── updater.ts │ ├── index.ts │ └── VesktopNative.ts ├── .env.example ├── tsconfig.json ├── .vscode └── settings.json ├── patches ├── electron-updater.patch └── arrpc@3.5.0.patch ├── README.md └── eslint.config.mjs /packages/libvesktop/.gitignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /static/dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | package-manager-strict=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .env 4 | .DS_Store 5 | .idea/ 6 | .pnpm-store/ -------------------------------------------------------------------------------- /static/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/tray.png -------------------------------------------------------------------------------- /static/splash.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/splash.webp -------------------------------------------------------------------------------- /static/badges/1.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/1.ico -------------------------------------------------------------------------------- /static/badges/10.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/10.ico -------------------------------------------------------------------------------- /static/badges/11.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/11.ico -------------------------------------------------------------------------------- /static/badges/2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/2.ico -------------------------------------------------------------------------------- /static/badges/3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/3.ico -------------------------------------------------------------------------------- /static/badges/4.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/4.ico -------------------------------------------------------------------------------- /static/badges/5.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/5.ico -------------------------------------------------------------------------------- /static/badges/6.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/6.ico -------------------------------------------------------------------------------- /static/badges/7.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/7.ico -------------------------------------------------------------------------------- /static/badges/8.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/8.ico -------------------------------------------------------------------------------- /static/badges/9.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/badges/9.ico -------------------------------------------------------------------------------- /static/tray/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/tray/tray.png -------------------------------------------------------------------------------- /static/tray/trayUnread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/tray/trayUnread.png -------------------------------------------------------------------------------- /static/tray/trayTemplate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/static/tray/trayTemplate.png -------------------------------------------------------------------------------- /packages/libvesktop/prebuilds/vesktop-x64.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/packages/libvesktop/prebuilds/vesktop-x64.node -------------------------------------------------------------------------------- /packages/libvesktop/prebuilds/vesktop-arm64.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vencord/Vesktop/HEAD/packages/libvesktop/prebuilds/vesktop-arm64.node -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | tabWidth: 4 2 | semi: true 3 | printWidth: 120 4 | trailingComma: none 5 | bracketSpacing: true 6 | arrowParens: avoid 7 | useTabs: false 8 | endOfLine: lf 9 | -------------------------------------------------------------------------------- /scripts/build/afterPack.mjs: -------------------------------------------------------------------------------- 1 | import { addAssetsCar } from "./addAssetsCar.mjs"; 2 | 3 | export default async function afterPack(context) { 4 | await addAssetsCar(context); 5 | } 6 | -------------------------------------------------------------------------------- /scripts/build/beforePack.mjs: -------------------------------------------------------------------------------- 1 | import { applyAppImageSandboxFix } from "./sandboxFix.mjs"; 2 | 3 | export default async function beforePack() { 4 | await applyAppImageSandboxFix(); 5 | } 6 | -------------------------------------------------------------------------------- /scripts/header.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) {year} {author} 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | -------------------------------------------------------------------------------- /packages/libvesktop/index.d.ts: -------------------------------------------------------------------------------- 1 | export function getAccentColor(): number | null; 2 | export function requestBackground(autoStart: boolean, commandLine: string[]): boolean; 3 | export function updateUnityLauncherCount(count: number): boolean; 4 | -------------------------------------------------------------------------------- /scripts/build/injectReact.mjs: -------------------------------------------------------------------------------- 1 | export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment"); 2 | export let VencordCreateElement = (...args) => 3 | (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args); 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Vencord Support Server 4 | url: https://discord.gg/D9uwnFnqmd 5 | about: "Need Help? Join our support server and ask in the #vesktop-support channel!" 6 | -------------------------------------------------------------------------------- /src/renderer/components/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | export * as ScreenShare from "./ScreenSharePicker"; 8 | -------------------------------------------------------------------------------- /scripts/utils/dotenv.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { config } from "dotenv"; 8 | 9 | config(); 10 | -------------------------------------------------------------------------------- /src/module.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | declare module "__patches__" { 8 | const never: never; 9 | export default never; 10 | } 11 | -------------------------------------------------------------------------------- /src/shared/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | export function sleep(ms: number): Promise { 8 | return new Promise(r => setTimeout(r, ms)); 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/logger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { Logger } from "@vencord/types/utils"; 8 | 9 | export const VesktopLogger = new Logger("Vesktop", "#d3869b"); 10 | -------------------------------------------------------------------------------- /src/renderer/common.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { findStoreLazy } from "@vencord/types/webpack"; 8 | 9 | export const MediaEngineStore = findStoreLazy("MediaEngineStore"); 10 | -------------------------------------------------------------------------------- /src/shared/utils/millis.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | export const enum Millis { 8 | SECOND = 1000, 9 | MINUTE = 60 * SECOND, 10 | HOUR = 60 * MINUTE, 11 | DAY = 24 * HOUR 12 | } 13 | -------------------------------------------------------------------------------- /scripts/startWatch.mts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import "./start"; 8 | 9 | import { spawnNodeModuleBin } from "./utils/spawn.mjs"; 10 | spawnNodeModuleBin("tsx", ["scripts/build/build.mts", "--", "--watch", "--dev"]); 11 | -------------------------------------------------------------------------------- /src/shared/paths.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { join } from "path"; 8 | 9 | export const STATIC_DIR = /* @__PURE__ */ join(__dirname, "..", "..", "static"); 10 | export const BADGE_DIR = /* @__PURE__ */ join(STATIC_DIR, "badges"); 11 | -------------------------------------------------------------------------------- /scripts/start.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-License-Identifier: GPL-3.0 3 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 4 | * Copyright (c) 2023 Vendicated and Vencord contributors 5 | */ 6 | 7 | import "./utils/dotenv"; 8 | 9 | import { spawnNodeModuleBin } from "./utils/spawn.mjs"; 10 | 11 | spawnNodeModuleBin("electron", [process.cwd(), ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]); 12 | -------------------------------------------------------------------------------- /packages/libvesktop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libvesktop", 3 | "main": "build/Release/vesktop.node", 4 | "types": "index.d.ts", 5 | "devDependencies": { 6 | "node-addon-api": "^8.5.0", 7 | "node-gyp": "^11.4.2" 8 | }, 9 | "scripts": { 10 | "build": "node-gyp configure build", 11 | "clean": "node-gyp clean", 12 | "test": "npm run build && node test.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/utils/fileExists.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { access, constants } from "fs/promises"; 8 | 9 | export async function fileExistsAsync(path: string) { 10 | return await access(path, constants.F_OK) 11 | .then(() => true) 12 | .catch(() => false); 13 | } 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # githubs api has a rate limit of 60/h if not authorised. 2 | # you may quickly hit that and get rate limited. To counteract this, you can provide a github token 3 | # here and it will be used. To do so, create a token at the following links and just leave 4 | # all permissions at the defaults (public repos read only, 0 permissions): 5 | # https://github.com/settings/personal-access-tokens/new 6 | GITHUB_TOKEN= 7 | 8 | ELECTRON_LAUNCH_FLAGS="--enable-source-maps --ozone-platform-hint=auto" -------------------------------------------------------------------------------- /src/shared/utils/guards.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | export function isTruthy(item: T): item is Exclude { 8 | return Boolean(item); 9 | } 10 | 11 | export function isNonNullish(item: T): item is Exclude { 12 | return item != null; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { EventEmitter } from "events"; 8 | 9 | import { UserAssetType } from "./userAssets"; 10 | 11 | export const AppEvents = new EventEmitter<{ 12 | appLoaded: []; 13 | userAssetChanged: [UserAssetType]; 14 | setTrayVariant: ["tray" | "trayUnread"]; 15 | }>(); 16 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | declare global { 8 | export var VesktopNative: typeof import("preload/VesktopNative").VesktopNative; 9 | export var Vesktop: typeof import("renderer/index"); 10 | export var VesktopPatchGlobals: any; 11 | 12 | export var IS_DEV: boolean; 13 | } 14 | 15 | export {}; 16 | -------------------------------------------------------------------------------- /packages/libvesktop/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | docker build -t libvesktop-builder -f Dockerfile . 5 | 6 | docker run --rm -v "$PWD":/src -w /src libvesktop-builder bash -c " 7 | set -e 8 | 9 | echo '=== Building x64 ===' 10 | npx node-gyp rebuild --arch=x64 11 | mv build/Release/vesktop.node prebuilds/vesktop-x64.node 12 | 13 | echo '=== Building arm64 ===' 14 | export CXX=aarch64-linux-gnu-g++ 15 | npx node-gyp rebuild --arch=arm64 16 | mv build/Release/vesktop.node prebuilds/vesktop-arm64.node 17 | " -------------------------------------------------------------------------------- /src/preload/splash.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { contextBridge, ipcRenderer } from "electron"; 8 | 9 | contextBridge.exposeInMainWorld("VesktopSplashNative", { 10 | onUpdateMessage(callback: (message: string) => void) { 11 | ipcRenderer.on("update-splash-message", (_, message: string) => callback(message)); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/main/vencordFilesDir.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { join } from "path"; 8 | 9 | import { SESSION_DATA_DIR } from "./constants"; 10 | import { State } from "./settings"; 11 | 12 | // this is in a separate file to avoid circular dependencies 13 | export const VENCORD_FILES_DIR = State.store.vencordDir || join(SESSION_DATA_DIR, "vencordFiles"); 14 | -------------------------------------------------------------------------------- /packages/libvesktop/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "libvesktop", 5 | "sources": [ "src/libvesktop.cc" ], 6 | "include_dirs": [ 7 | ") { 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/patches/enableNotificationsByDefault.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { addPatch } from "./shared"; 8 | 9 | addPatch({ 10 | patches: [ 11 | { 12 | find: '"NotificationSettingsStore', 13 | replacement: { 14 | match: /\.isPlatformEmbedded(?=\?\i\.\i\.ALL)/g, 15 | replace: "$&||true" 16 | } 17 | } 18 | ] 19 | }); 20 | -------------------------------------------------------------------------------- /src/renderer/patches/hideDownloadAppsButton.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { addPatch } from "./shared"; 8 | 9 | addPatch({ 10 | patches: [ 11 | { 12 | find: '"app-download-button"', 13 | replacement: { 14 | match: /return(?=.{0,50}id:"app-download-button")/, 15 | replace: "return null;return" 16 | } 17 | } 18 | ] 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/utils/spawn.mts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { spawn as spaaawn, SpawnOptions } from "child_process"; 8 | import { join } from "path"; 9 | 10 | const EXT = process.platform === "win32" ? ".cmd" : ""; 11 | 12 | const OPTS: SpawnOptions = { 13 | stdio: "inherit" 14 | }; 15 | 16 | export function spawnNodeModuleBin(bin: string, args: string[]) { 17 | spaaawn(join("node_modules", ".bin", bin + EXT), args, OPTS); 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/components/settings/UserAssets.css: -------------------------------------------------------------------------------- 1 | .vcd-user-assets { 2 | display: flex; 3 | margin-block: 1em 2em; 4 | flex-direction: column; 5 | gap: 1em; 6 | } 7 | 8 | .vcd-user-assets-asset { 9 | display: flex; 10 | margin-top: 0.5em; 11 | align-items: center; 12 | gap: 1em; 13 | } 14 | 15 | .vcd-user-assets-actions { 16 | display: grid; 17 | width: 100%; 18 | gap: 0.5em; 19 | margin-bottom: auto; 20 | } 21 | 22 | .vcd-user-assets-buttons { 23 | display: flex; 24 | gap: 0.5em; 25 | } 26 | 27 | .vcd-user-assets-image { 28 | height: 7.5em; 29 | width: 7.5em; 30 | object-fit: contain; 31 | } -------------------------------------------------------------------------------- /src/shared/utils/text.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | export function stripIndent(strings: TemplateStringsArray, ...values: any[]) { 8 | const string = String.raw({ raw: strings }, ...values); 9 | 10 | const match = string.match(/^[ \t]*(?=\S)/gm); 11 | if (!match) return string.trim(); 12 | 13 | const minIndent = match.reduce((r, a) => Math.min(r, a.length), Infinity); 14 | return string.replace(new RegExp(`^[ \\t]{${minIndent}}`, "gm"), "").trim(); 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/patches/streamerMode.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { addPatch } from "./shared"; 8 | 9 | addPatch({ 10 | patches: [ 11 | { 12 | find: ".STREAMER_MODE_ENABLE,", 13 | replacement: { 14 | // remove if (platformEmbedded) check from streamer mode toggle 15 | match: /if\(\i\.\i\)(?=return.{0,200}?"autoToggle")/g, 16 | replace: "" 17 | } 18 | } 19 | ] 20 | }); 21 | -------------------------------------------------------------------------------- /src/preload/typedIpc.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { ipcRenderer } from "electron"; 8 | import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents"; 9 | 10 | export function invoke(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) { 11 | return ipcRenderer.invoke(event, ...args) as Promise; 12 | } 13 | 14 | export function sendSync(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) { 15 | return ipcRenderer.sendSync(event, ...args) as T; 16 | } 17 | -------------------------------------------------------------------------------- /src/shared/utils/once.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | /** 8 | * Wraps the given function so that it can only be called once 9 | * @param fn Function to wrap 10 | * @returns New function that can only be called once 11 | */ 12 | export function once(fn: T): T { 13 | let called = false; 14 | return function (this: any, ...args: any[]) { 15 | if (called) return; 16 | called = true; 17 | return fn.apply(this, args); 18 | } as any; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/utils/isPathInDirectory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { resolve, sep } from "path"; 8 | 9 | export function isPathInDirectory(filePath: string, directory: string) { 10 | const resolvedPath = resolve(filePath); 11 | const resolvedDirectory = resolve(directory); 12 | 13 | const normalizedDirectory = resolvedDirectory.endsWith(sep) ? resolvedDirectory : resolvedDirectory + sep; 14 | 15 | return resolvedPath.startsWith(normalizedDirectory) || resolvedPath === resolvedDirectory; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "lib": ["DOM", "DOM.Iterable", "esnext", "esnext.array", "esnext.asynciterable", "esnext.symbol"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "target": "ESNEXT", 11 | "jsx": "preserve", 12 | 13 | // @vencord/types has some errors for now 14 | "skipLibCheck": true, 15 | 16 | "baseUrl": "./src/", 17 | 18 | "typeRoots": ["./node_modules/@types", "./node_modules/@vencord"] 19 | }, 20 | "include": ["src/**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/libvesktop/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for building both x64 and arm64 on an old distro for maximum compatibility. 2 | 3 | # ubuntu20 is dead but debian11 is still supported for now 4 | FROM debian:11 5 | ENV DEBIAN_FRONTEND=noninteractive 6 | 7 | RUN dpkg --add-architecture arm64 8 | 9 | RUN apt-get update && apt-get install -y \ 10 | build-essential python3 curl pkg-config \ 11 | g++-aarch64-linux-gnu libglib2.0-dev:amd64 libglib2.0-dev:arm64 \ 12 | ca-certificates \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ 16 | && apt-get update && apt-get install -y nodejs \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | WORKDIR /src 20 | -------------------------------------------------------------------------------- /src/renderer/patches/shared.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { Patch } from "@vencord/types/utils/types"; 8 | 9 | window.VesktopPatchGlobals = {}; 10 | 11 | interface PatchData { 12 | patches: Omit[]; 13 | [key: string]: any; 14 | } 15 | 16 | export function addPatch

(p: P) { 17 | const { patches, ...globals } = p; 18 | 19 | for (const patch of patches) { 20 | Vencord.Plugins.addPatch(patch, "Vesktop", "VesktopPatchGlobals"); 21 | } 22 | 23 | Object.assign(VesktopPatchGlobals, globals); 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/patches/hideSwitchDevice.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { addPatch } from "./shared"; 8 | 9 | addPatch({ 10 | patches: [ 11 | { 12 | find: "lastOutputSystemDevice.justChanged", 13 | replacement: { 14 | match: /(\i)\.\i\.getState\(\).neverShowModal/, 15 | replace: "$& || $self.shouldIgnoreDevice($1)" 16 | } 17 | } 18 | ], 19 | 20 | shouldIgnoreDevice(state: any) { 21 | return Object.keys(state?.default?.lastDeviceConnected ?? {})?.[0] === "vencord-screen-share"; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/shared/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | /** 8 | * Returns a new function that will only be called after the given delay. 9 | * Subsequent calls will cancel the previous timeout and start a new one from 0 10 | * 11 | * Useful for grouping multiple calls into one 12 | */ 13 | export function debounce(func: T, delay = 300): T { 14 | let timeout: NodeJS.Timeout; 15 | return function (...args: any[]) { 16 | clearTimeout(timeout); 17 | timeout = setTimeout(() => { 18 | func(...args); 19 | }, delay); 20 | } as any; 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | // Discord deletes this from the window so we need to capture it in a variable 8 | export const { localStorage } = window; 9 | 10 | export const isFirstRun = (() => { 11 | const key = "VCD_FIRST_RUN"; 12 | if (localStorage.getItem(key) !== null) return false; 13 | localStorage.setItem(key, "false"); 14 | return true; 15 | })(); 16 | 17 | const { platform } = navigator; 18 | 19 | export const isWindows = platform.startsWith("Win"); 20 | export const isMac = platform.startsWith("Mac"); 21 | export const isLinux = platform.startsWith("Linux"); 22 | -------------------------------------------------------------------------------- /src/renderer/patches/hideVenmicInput.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { addPatch } from "./shared"; 8 | 9 | addPatch({ 10 | patches: [ 11 | { 12 | find: 'setSinkId"in', 13 | replacement: { 14 | match: /return (\i)\?navigator\.mediaDevices\.enumerateDevices/, 15 | replace: "return $1 ? $self.filteredDevices" 16 | } 17 | } 18 | ], 19 | 20 | async filteredDevices() { 21 | const original = await navigator.mediaDevices.enumerateDevices(); 22 | return original.filter(x => x.label !== "vencord-screen-share"); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/renderer/patches/taskBarFlash.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { Settings } from "renderer/settings"; 8 | 9 | import { addPatch } from "./shared"; 10 | 11 | addPatch({ 12 | patches: [ 13 | { 14 | find: ".flashFrame(!0)", 15 | replacement: { 16 | match: /(\i)&&\i\.\i\.taskbarFlash&&\i\.\i\.flashFrame\(!0\)/, 17 | replace: "$self.flashFrame()" 18 | } 19 | } 20 | ], 21 | 22 | flashFrame() { 23 | if (Settings.store.enableTaskbarFlashing) { 24 | VesktopNative.win.flashFrame(true); 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /.github/workflows/winget-submission.yml: -------------------------------------------------------------------------------- 1 | name: Submit to Winget Community Repo 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | inputs: 8 | tag: 9 | type: string 10 | description: The release tag to submit 11 | required: true 12 | 13 | jobs: 14 | winget: 15 | name: Publish winget package 16 | runs-on: windows-latest 17 | steps: 18 | - name: Submit package to Winget Community Repo 19 | uses: vedantmgoyal2009/winget-releaser@0db4f0a478166abd0fa438c631849f0b8dcfb99f 20 | with: 21 | identifier: Vencord.Vesktop 22 | token: ${{ secrets.WINGET_PAT }} 23 | installers-regex: '\.exe$' 24 | release-tag: ${{ inputs.tag || github.event.release.tag_name }} 25 | fork-user: shiggybot 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[javascript]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[typescriptreact]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "[javascriptreact]": { 16 | "editor.defaultFormatter": "esbenp.prettier-vscode" 17 | }, 18 | "[json]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode" 20 | }, 21 | "[jsonc]": { 22 | "editor.defaultFormatter": "esbenp.prettier-vscode" 23 | }, 24 | "cSpell.words": ["Vesktop"] 25 | } 26 | -------------------------------------------------------------------------------- /scripts/build/addAssetsCar.mjs: -------------------------------------------------------------------------------- 1 | import { copyFile, readdir } from "fs/promises"; 2 | 3 | /** 4 | * @param {{ 5 | * readonly appOutDir: string; 6 | * readonly arch: Arch; 7 | * readonly electronPlatformName: string; 8 | * readonly outDir: string; 9 | * readonly packager: PlatformPackager; 10 | * readonly targets: Target[]; 11 | * }} context 12 | */ 13 | export async function addAssetsCar({ appOutDir }) { 14 | if (process.platform !== "darwin") return; 15 | 16 | const appName = (await readdir(appOutDir)).find(item => item.endsWith(".app")); 17 | 18 | if (!appName) { 19 | console.warn(`Could not find .app directory in ${appOutDir}. Skipping adding assets.car`); 20 | return; 21 | } 22 | 23 | await copyFile("build/Assets.car", `${appOutDir}/${appName}/Contents/Resources/Assets.car`); 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/fixes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { localStorage } from "./utils"; 8 | 9 | // Make clicking Notifications focus the window 10 | const originalSetOnClick = Object.getOwnPropertyDescriptor(Notification.prototype, "onclick")!.set!; 11 | Object.defineProperty(Notification.prototype, "onclick", { 12 | set(onClick) { 13 | originalSetOnClick.call(this, function (this: unknown) { 14 | onClick.apply(this, arguments); 15 | VesktopNative.win.focus(); 16 | }); 17 | }, 18 | configurable: true 19 | }); 20 | 21 | // Hide "Download Discord Desktop now!!!!" banner 22 | localStorage.setItem("hideNag", "true"); 23 | -------------------------------------------------------------------------------- /src/renderer/components/settings/settings.css: -------------------------------------------------------------------------------- 1 | .vcd-settings-button-grid { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr; 4 | gap: 0.5em; 5 | margin-top: 0.5em; 6 | } 7 | 8 | .vcd-settings-title { 9 | margin-bottom: 32px; 10 | } 11 | 12 | .vcd-settings-category { 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | .vcd-settings-category-title { 18 | margin-bottom: 16px; 19 | } 20 | 21 | .vcd-settings-category-content { 22 | display: flex; 23 | flex-direction: column; 24 | gap: 24px; 25 | } 26 | 27 | .vcd-settings-category-divider { 28 | margin-top: 32px; 29 | margin-bottom: 32px; 30 | } 31 | 32 | .vcd-settings-switch { 33 | margin-bottom: 0; 34 | } 35 | 36 | .vcd-settings-updater-card { 37 | padding: 1em; 38 | margin-bottom: 1em; 39 | display: grid; 40 | gap: 0.5em; 41 | } -------------------------------------------------------------------------------- /static/views/common.css: -------------------------------------------------------------------------------- 1 | :root { 2 | color-scheme: light dark; 3 | 4 | --bg: light-dark(white, hsl(223 6.7% 20.6%)); 5 | --fg: light-dark(black, white); 6 | --fg-secondary: light-dark(#313338, #b5bac1); 7 | --fg-semi-trans: light-dark(rgb(0 0 0 / 0.2), rgb(255 255 255 / 0.2)); 8 | --link: light-dark(#006ce7, #00a8fc); 9 | --link-hover: light-dark(#005bb5, #0086c3); 10 | } 11 | 12 | html, 13 | body { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | body { 19 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, 20 | "Open Sans", "Helvetica Neue", sans-serif; 21 | background: var(--bg); 22 | color: var(--fg); 23 | } 24 | 25 | a { 26 | color: var(--link); 27 | transition: color 0.2s linear; 28 | } 29 | 30 | a:hover { 31 | color: var(--link-hover); 32 | } -------------------------------------------------------------------------------- /src/renderer/patches/platformClass.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { Settings } from "renderer/settings"; 8 | import { isMac } from "renderer/utils"; 9 | 10 | import { addPatch } from "./shared"; 11 | 12 | addPatch({ 13 | patches: [ 14 | { 15 | find: "platform-web", 16 | replacement: { 17 | match: /(?<=" platform-overlay"\):)\i/, 18 | replace: "$self.getPlatformClass()" 19 | } 20 | } 21 | ], 22 | 23 | getPlatformClass() { 24 | if (Settings.store.customTitleBar) return "platform-win"; 25 | if (isMac) return "platform-osx"; 26 | return "platform-web"; 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /patches/electron-updater.patch: -------------------------------------------------------------------------------- 1 | diff --git a/out/RpmUpdater.js b/out/RpmUpdater.js 2 | index 563187bb18cb0bd154dff6620cb62b8c8f534cd6..d91594026c2bac9cc78ef3b1183df3241d7d9624 100644 3 | --- a/out/RpmUpdater.js 4 | +++ b/out/RpmUpdater.js 5 | @@ -32,7 +32,10 @@ class RpmUpdater extends BaseUpdater_1.BaseUpdater { 6 | const sudo = this.wrapSudo(); 7 | // pkexec doesn't want the command to be wrapped in " quotes 8 | const wrapper = /pkexec/i.test(sudo) ? "" : `"`; 9 | - const packageManager = this.spawnSyncLog("which zypper"); 10 | + let packageManager; 11 | + try { 12 | + packageManager = this.spawnSyncLog("which zypper"); 13 | + } catch {} 14 | const installerPath = this.installerPath; 15 | if (installerPath == null) { 16 | this.dispatchError(new Error("No valid update available, can't quit and install")); 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json 16 | 17 | - name: Use Node.js 20 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | cache: "pnpm" 22 | 23 | - name: Install dependencies 24 | run: pnpm install --frozen-lockfile 25 | 26 | - name: Run tests 27 | run: pnpm test 28 | 29 | - name: Test if it compiles 30 | run: | 31 | pnpm build 32 | pnpm build --dev 33 | -------------------------------------------------------------------------------- /packages/libvesktop/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {typeof import(".")} 3 | */ 4 | const libVesktop = require("."); 5 | const test = require("node:test"); 6 | const assert = require("node:assert/strict"); 7 | 8 | test("getAccentColor should return a number", () => { 9 | const color = libVesktop.getAccentColor(); 10 | assert.strictEqual(typeof color, "number"); 11 | }); 12 | 13 | test("updateUnityLauncherCount should return true (success)", () => { 14 | assert.strictEqual(libVesktop.updateUnityLauncherCount(5), true); 15 | assert.strictEqual(libVesktop.updateUnityLauncherCount(0), true); 16 | assert.strictEqual(libVesktop.updateUnityLauncherCount(10), true); 17 | }); 18 | 19 | test("requestBackground should return true (success)", () => { 20 | assert.strictEqual(libVesktop.requestBackground(true, ["bash"]), true); 21 | assert.strictEqual(libVesktop.requestBackground(false, []), true); 22 | }); 23 | -------------------------------------------------------------------------------- /src/main/about.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { app, BrowserWindow } from "electron"; 8 | 9 | import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; 10 | import { loadView } from "./vesktopStatic"; 11 | 12 | export async function createAboutWindow() { 13 | const height = 750; 14 | const width = height * (4 / 3); 15 | 16 | const about = new BrowserWindow({ 17 | center: true, 18 | autoHideMenuBar: true, 19 | height, 20 | width 21 | }); 22 | 23 | makeLinksOpenExternally(about); 24 | 25 | const data = new URLSearchParams({ 26 | APP_VERSION: app.getVersion() 27 | }); 28 | 29 | loadView(about, "about.html", data); 30 | 31 | return about; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/vesktopProtocol.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { app, protocol } from "electron"; 8 | 9 | import { handleVesktopAssetsProtocol } from "./userAssets"; 10 | import { handleVesktopStaticProtocol } from "./vesktopStatic"; 11 | 12 | app.whenReady().then(() => { 13 | protocol.handle("vesktop", async req => { 14 | const url = new URL(req.url); 15 | 16 | switch (url.hostname) { 17 | case "assets": 18 | return handleVesktopAssetsProtocol(url.pathname, req); 19 | case "static": 20 | return handleVesktopStaticProtocol(url.pathname, req); 21 | default: 22 | return new Response(null, { status: 404 }); 23 | } 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/renderer/components/settings/NotificationBadgeToggle.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { setBadge } from "renderer/appBadge"; 8 | 9 | import { SettingsComponent } from "./Settings"; 10 | import { VesktopSettingsSwitch } from "./VesktopSettingsSwitch"; 11 | 12 | export const NotificationBadgeToggle: SettingsComponent = ({ settings }) => { 13 | return ( 14 | { 19 | settings.appBadge = v; 20 | if (v) setBadge(); 21 | else VesktopNative.app.setBadgeCount(0); 22 | }} 23 | /> 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/dev-issue.yml: -------------------------------------------------------------------------------- 1 | name: Vesktop Developer Issue 2 | description: Reserved for Vesktop Developers. Join our support server for support. 3 | 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # This form is reserved for Vesktop Developers. Do not open an issue. 9 | 10 | Instead, use the [#vesktop-support channel](https://discord.com/channels/1015060230222131221/1345457031426871417) on our [Discord server](https://vencord.dev/discord) for help and reporting issues. 11 | 12 | Your issue will be closed immediately with no comment and you will be blocked if you ignore this. 13 | 14 | This is because 99% of issues are not actually bugs, but rather user or system issues and it adds a lot of noise to our development process. 15 | - type: textarea 16 | id: content 17 | attributes: 18 | label: Content 19 | validations: 20 | required: true 21 | -------------------------------------------------------------------------------- /scripts/build/includeDirPlugin.mts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "esbuild"; 2 | import { readdir } from "fs/promises"; 3 | 4 | const makeImportAllCode = (files: string[]) => 5 | files.map(f => `require("./${f.replace(/\.[cm]?[tj]sx?$/, "")}")`).join("\n"); 6 | 7 | const makeImportDirRecursiveCode = (dir: string) => readdir(dir).then(files => makeImportAllCode(files)); 8 | 9 | export function includeDirPlugin(namespace: string, path: string): Plugin { 10 | return { 11 | name: `include-dir-plugin:${namespace}`, 12 | setup(build) { 13 | const filter = new RegExp(`^__${namespace}__$`); 14 | 15 | build.onResolve({ filter }, args => ({ path: args.path, namespace })); 16 | 17 | build.onLoad({ filter, namespace }, async args => { 18 | return { 19 | contents: await makeImportDirRecursiveCode(path), 20 | resolveDir: path 21 | }; 22 | }); 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/mediaPermissions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { session, systemPreferences } from "electron"; 8 | 9 | export function registerMediaPermissionsHandler() { 10 | if (process.platform !== "darwin") return; 11 | 12 | session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => { 13 | let granted = true; 14 | 15 | if ("mediaTypes" in details) { 16 | if (details.mediaTypes?.includes("audio")) { 17 | granted &&= await systemPreferences.askForMediaAccess("microphone"); 18 | } 19 | if (details.mediaTypes?.includes("video")) { 20 | granted &&= await systemPreferences.askForMediaAccess("camera"); 21 | } 22 | } 23 | 24 | callback(granted); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/arrpc/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | export type ArRpcEvent = ArRpcActivityEvent | ArRpcInviteEvent | ArRpcLinkEvent; 8 | export type ArRpcHostEvent = ArRpcHostAckInviteEvent | ArRpcHostAckLinkEvent; 9 | 10 | export interface ArRpcActivityEvent { 11 | type: "activity"; 12 | nonce: string; 13 | data: string; 14 | } 15 | 16 | export interface ArRpcInviteEvent { 17 | type: "invite"; 18 | nonce: string; 19 | data: string; 20 | } 21 | 22 | export interface ArRpcLinkEvent { 23 | type: "link"; 24 | nonce: string; 25 | data: any; 26 | } 27 | 28 | export interface ArRpcHostAckInviteEvent { 29 | type: "ack-invite"; 30 | nonce: string; 31 | data: boolean; 32 | } 33 | 34 | export interface ArRpcHostAckLinkEvent { 35 | type: "ack-link"; 36 | nonce: string; 37 | data: boolean; 38 | } 39 | -------------------------------------------------------------------------------- /src/renderer/patches/windowMethods.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { addPatch } from "./shared"; 8 | 9 | addPatch({ 10 | patches: [ 11 | { 12 | find: ",setSystemTrayApplications", 13 | replacement: [ 14 | { 15 | match: /\i\.window\.(close|minimize|maximize)/g, 16 | replace: `VesktopNative.win.$1` 17 | }, 18 | { 19 | match: /(focus(\(\i\)){).{0,150}?\.focus\(\i,\i\)/, 20 | replace: "$1VesktopNative.win.focus$2" 21 | }, 22 | { 23 | match: /,getEnableHardwareAcceleration/, 24 | replace: "$&:VesktopNative.app.getEnableHardwareAcceleration,_oldGetEnableHardwareAcceleration" 25 | } 26 | ] 27 | } 28 | ] 29 | }); 30 | -------------------------------------------------------------------------------- /patches/arrpc@3.5.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/process/index.js b/src/process/index.js 2 | index 389b0845256a34b4536d6da99edb00d17f13a6b4..f17a0ac687e9110ebfd33cb91fd2f6250d318643 100644 3 | --- a/src/process/index.js 4 | +++ b/src/process/index.js 5 | @@ -5,8 +5,20 @@ import fs from 'node:fs'; 6 | import { dirname, join } from 'path'; 7 | import { fileURLToPath } from 'url'; 8 | 9 | -const __dirname = dirname(fileURLToPath(import.meta.url)); 10 | -const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8')); 11 | +const DetectableDB = require('./detectable.json'); 12 | +DetectableDB.push( 13 | + { 14 | + aliases: ["Obs"], 15 | + executables: [ 16 | + { is_launcher: false, name: "obs", os: "linux" }, 17 | + { is_launcher: false, name: "obs.exe", os: "win32" }, 18 | + { is_launcher: false, name: "obs.app", os: "darwin" } 19 | + ], 20 | + hook: true, 21 | + id: "STREAMERMODE", 22 | + name: "OBS" 23 | + } 24 | +); 25 | 26 | import * as Natives from './native/index.js'; 27 | const Native = Natives[process.platform]; 28 | -------------------------------------------------------------------------------- /src/renderer/components/settings/OutdatedVesktopWarning.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { Button, Card, HeadingTertiary, Paragraph } from "@vencord/types/components"; 8 | import { useAwaiter } from "@vencord/types/utils"; 9 | 10 | import { cl } from "./Settings"; 11 | 12 | export function OutdatedVesktopWarning() { 13 | const [isOutdated] = useAwaiter(VesktopNative.app.isOutdated); 14 | 15 | if (!isOutdated) return null; 16 | 17 | return ( 18 | 19 | Your Vesktop is outdated! 20 | Staying up to date is important for security and stability. 21 | 22 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/preload/updater.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { contextBridge, ipcRenderer } from "electron"; 8 | import type { UpdateInfo } from "electron-updater"; 9 | import { UpdaterIpcEvents } from "shared/IpcEvents"; 10 | 11 | import { invoke } from "./typedIpc"; 12 | 13 | contextBridge.exposeInMainWorld("VesktopUpdaterNative", { 14 | getData: () => invoke(UpdaterIpcEvents.GET_DATA), 15 | installUpdate: () => invoke(UpdaterIpcEvents.INSTALL), 16 | onProgress: (cb: (percent: number) => void) => { 17 | ipcRenderer.on(UpdaterIpcEvents.DOWNLOAD_PROGRESS, (_, percent: number) => cb(percent)); 18 | }, 19 | onError: (cb: (message: string) => void) => { 20 | ipcRenderer.on(UpdaterIpcEvents.ERROR, (_, message: string) => cb(message)); 21 | }, 22 | snoozeUpdate: () => invoke(UpdaterIpcEvents.SNOOZE_UPDATE), 23 | ignoreUpdate: () => invoke(UpdaterIpcEvents.IGNORE_UPDATE) 24 | }); 25 | -------------------------------------------------------------------------------- /src/main/vesktopStatic.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { BrowserWindow, net } from "electron"; 8 | import { join } from "path"; 9 | import { pathToFileURL } from "url"; 10 | 11 | import { isPathInDirectory } from "./utils/isPathInDirectory"; 12 | 13 | const STATIC_DIR = join(__dirname, "..", "..", "static"); 14 | 15 | export async function handleVesktopStaticProtocol(path: string, req: Request) { 16 | const fullPath = join(STATIC_DIR, path); 17 | if (!isPathInDirectory(fullPath, STATIC_DIR)) { 18 | return new Response(null, { status: 404 }); 19 | } 20 | 21 | return net.fetch(pathToFileURL(fullPath).href); 22 | } 23 | 24 | export function loadView(browserWindow: BrowserWindow, view: string, params?: URLSearchParams) { 25 | const url = new URL(`vesktop://static/views/${view}`); 26 | if (params) { 27 | url.search = params.toString(); 28 | } 29 | 30 | return browserWindow.loadURL(url.toString()); 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import "./themedSplash"; 8 | import "./ipcCommands"; 9 | import "./appBadge"; 10 | import "./fixes"; 11 | import "./arrpc"; 12 | import "__patches__"; // auto generated by the build script 13 | 14 | export * as Components from "./components"; 15 | 16 | import SettingsUi from "./components/settings/Settings"; 17 | import { VesktopLogger } from "./logger"; 18 | import { Settings } from "./settings"; 19 | export { Settings }; 20 | 21 | import type SettingsPlugin from "@vencord/types/plugins/_core/settings"; 22 | 23 | VesktopLogger.log("read if cute :3"); 24 | VesktopLogger.log("Vesktop v" + VesktopNative.app.getVersion()); 25 | 26 | // TODO 27 | const customSettingsSections = (Vencord.Plugins.plugins.Settings as typeof SettingsPlugin).customSections; 28 | 29 | customSettingsSections.push(() => ({ 30 | section: "Vesktop", 31 | label: "Vesktop Settings", 32 | element: SettingsUi, 33 | className: "vc-vesktop-settings" 34 | })); 35 | -------------------------------------------------------------------------------- /src/renderer/components/settings/DiscordBranchPicker.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { Select } from "@vencord/types/webpack/common"; 8 | 9 | import { SimpleErrorBoundary } from "../SimpleErrorBoundary"; 10 | import { SettingsComponent } from "./Settings"; 11 | 12 | export const DiscordBranchPicker: SettingsComponent = ({ settings }) => { 13 | return ( 14 | 15 | (settings.transparencyOption = v)} 45 | isSelected={v => v === settings.transparencyOption} 46 | serialize={s => s} 47 | /> 48 | 49 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/main/arrpc/worker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import Server from "arrpc"; 8 | import { randomUUID } from "crypto"; 9 | import { MessagePort, workerData } from "worker_threads"; 10 | 11 | import { ArRpcEvent, ArRpcHostEvent } from "./types"; 12 | 13 | let server: any; 14 | 15 | type InviteCallback = (valid: boolean) => void; 16 | type LinkCallback = InviteCallback; 17 | 18 | const inviteCallbacks = new Map(); 19 | const linkCallbacks = new Map(); 20 | 21 | (async function () { 22 | const { workerPort } = workerData as { workerPort: MessagePort }; 23 | 24 | server = await new Server(); 25 | 26 | server.on("activity", (data: any) => { 27 | const event: ArRpcEvent = { 28 | type: "activity", 29 | data: JSON.stringify(data), 30 | nonce: randomUUID() 31 | }; 32 | workerPort.postMessage(event); 33 | }); 34 | 35 | server.on("invite", (invite: string, callback: InviteCallback) => { 36 | const nonce = randomUUID(); 37 | inviteCallbacks.set(nonce, callback); 38 | 39 | const event: ArRpcEvent = { 40 | type: "invite", 41 | data: invite, 42 | nonce 43 | }; 44 | workerPort.postMessage(event); 45 | }); 46 | 47 | server.on("link", async (data: any, callback: LinkCallback) => { 48 | const nonce = randomUUID(); 49 | linkCallbacks.set(nonce, callback); 50 | 51 | const event: ArRpcEvent = { 52 | type: "link", 53 | data, 54 | nonce 55 | }; 56 | workerPort.postMessage(event); 57 | }); 58 | 59 | workerPort.on("message", (e: ArRpcHostEvent) => { 60 | switch (e.type) { 61 | case "ack-invite": { 62 | inviteCallbacks.get(e.nonce)?.(e.data); 63 | inviteCallbacks.delete(e.nonce); 64 | break; 65 | } 66 | case "ack-link": { 67 | linkCallbacks.get(e.nonce)?.(e.data); 68 | linkCallbacks.delete(e.nonce); 69 | break; 70 | } 71 | } 72 | }); 73 | })(); 74 | -------------------------------------------------------------------------------- /src/main/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { app } from "electron"; 8 | import { existsSync, mkdirSync } from "fs"; 9 | import { dirname, join } from "path"; 10 | 11 | import { CommandLine } from "./cli"; 12 | 13 | const vesktopDir = dirname(process.execPath); 14 | 15 | export const PORTABLE = 16 | process.platform === "win32" && 17 | !process.execPath.toLowerCase().endsWith("electron.exe") && 18 | !existsSync(join(vesktopDir, "Uninstall Vesktop.exe")); 19 | 20 | export const DATA_DIR = 21 | process.env.VENCORD_USER_DATA_DIR || (PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("userData"))); 22 | 23 | mkdirSync(DATA_DIR, { recursive: true }); 24 | 25 | export const SESSION_DATA_DIR = join(DATA_DIR, "sessionData"); 26 | app.setPath("sessionData", SESSION_DATA_DIR); 27 | 28 | export const VENCORD_SETTINGS_DIR = join(DATA_DIR, "settings"); 29 | mkdirSync(VENCORD_SETTINGS_DIR, { recursive: true }); 30 | export const VENCORD_QUICKCSS_FILE = join(VENCORD_SETTINGS_DIR, "quickCss.css"); 31 | export const VENCORD_SETTINGS_FILE = join(VENCORD_SETTINGS_DIR, "settings.json"); 32 | export const VENCORD_THEMES_DIR = join(DATA_DIR, "themes"); 33 | 34 | export const USER_AGENT = `Vesktop/${app.getVersion()} (https://github.com/Vencord/Vesktop)`; 35 | 36 | // dimensions shamelessly stolen from Discord Desktop :3 37 | export const MIN_WIDTH = 940; 38 | export const MIN_HEIGHT = 500; 39 | export const DEFAULT_WIDTH = 1280; 40 | export const DEFAULT_HEIGHT = 720; 41 | 42 | export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"]; 43 | 44 | const VersionString = `AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome.split(".")[0]}.0.0.0 Safari/537.36`; 45 | const BrowserUserAgents = { 46 | darwin: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ${VersionString}`, 47 | linux: `Mozilla/5.0 (X11; Linux x86_64) ${VersionString}`, 48 | windows: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) ${VersionString}` 49 | }; 50 | 51 | export const BrowserUserAgent = 52 | CommandLine.values["user-agent"] || 53 | BrowserUserAgents[CommandLine.values["user-agent-os"] || process.platform] || 54 | BrowserUserAgents.windows; 55 | 56 | export const enum MessageBoxChoice { 57 | Default, 58 | Cancel 59 | } 60 | 61 | export const IS_FLATPAK = process.env.FLATPAK_ID !== undefined; 62 | -------------------------------------------------------------------------------- /scripts/build/sandboxFix.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | // Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js 8 | 9 | import fs from "fs/promises"; 10 | import path from "path"; 11 | import AppImageTarget from "app-builder-lib/out/targets/AppImageTarget.js"; 12 | 13 | let isApplied = false; 14 | 15 | export async function applyAppImageSandboxFix() { 16 | if (process.platform !== "linux") { 17 | // this fix is only required on linux 18 | return; 19 | } 20 | 21 | if (isApplied) return; 22 | isApplied = true; 23 | 24 | const oldBuildMethod = AppImageTarget.default.prototype.build; 25 | AppImageTarget.default.prototype.build = async function (...args) { 26 | console.log("Running AppImage builder hook", args); 27 | const oldPath = args[0]; 28 | const newPath = oldPath + "-appimage-sandbox-fix"; 29 | // just in case 30 | try { 31 | await fs.rm(newPath, { 32 | recursive: true 33 | }); 34 | } catch {} 35 | 36 | console.log("Copying to apply appimage fix", oldPath, newPath); 37 | await fs.cp(oldPath, newPath, { 38 | recursive: true 39 | }); 40 | args[0] = newPath; 41 | 42 | const executable = path.join(newPath, this.packager.executableName); 43 | 44 | const loaderScript = ` 45 | #!/usr/bin/env bash 46 | 47 | SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd )" 48 | IS_STEAMOS=0 49 | 50 | if [[ "$SteamOS" == "1" && "$SteamGamepadUI" == "1" ]]; then 51 | echo "Running Vesktop on SteamOS, disabling sandbox" 52 | IS_STEAMOS=1 53 | fi 54 | 55 | exec "$SCRIPT_DIR/${this.packager.executableName}.bin" "$([ "$IS_STEAMOS" == 1 ] && echo '--no-sandbox')" "$@" 56 | `.trim(); 57 | 58 | try { 59 | await fs.rename(executable, executable + ".bin"); 60 | await fs.writeFile(executable, loaderScript); 61 | await fs.chmod(executable, 0o755); 62 | } catch (e) { 63 | console.error("failed to create loder for sandbox fix: " + e.message); 64 | throw new Error("Failed to create loader for sandbox fix"); 65 | } 66 | 67 | const ret = await oldBuildMethod.apply(this, args); 68 | 69 | await fs.rm(newPath, { 70 | recursive: true 71 | }); 72 | 73 | return ret; 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/main/utils/makeLinksOpenExternally.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { BrowserWindow, shell } from "electron"; 8 | import { DISCORD_HOSTNAMES } from "main/constants"; 9 | 10 | import { Settings } from "../settings"; 11 | import { createOrFocusPopup, setupPopout } from "./popout"; 12 | import { execSteamURL, isDeckGameMode, steamOpenURL } from "./steamOS"; 13 | 14 | export function handleExternalUrl(url: string, protocol?: string): { action: "deny" | "allow" } { 15 | if (protocol == null) { 16 | try { 17 | protocol = new URL(url).protocol; 18 | } catch { 19 | return { action: "deny" }; 20 | } 21 | } 22 | 23 | switch (protocol) { 24 | case "http:": 25 | case "https:": 26 | if (Settings.store.openLinksWithElectron) { 27 | return { action: "allow" }; 28 | } 29 | // eslint-disable-next-line no-fallthrough 30 | case "mailto:": 31 | case "spotify:": 32 | if (isDeckGameMode) { 33 | steamOpenURL(url); 34 | } else { 35 | shell.openExternal(url); 36 | } 37 | break; 38 | case "steam:": 39 | if (isDeckGameMode) { 40 | execSteamURL(url); 41 | } else { 42 | shell.openExternal(url); 43 | } 44 | break; 45 | } 46 | 47 | return { action: "deny" }; 48 | } 49 | 50 | export function makeLinksOpenExternally(win: BrowserWindow) { 51 | win.webContents.setWindowOpenHandler(({ url, frameName, features }) => { 52 | try { 53 | var { protocol, hostname, pathname, searchParams } = new URL(url); 54 | } catch { 55 | return { action: "deny" }; 56 | } 57 | 58 | if (frameName.startsWith("DISCORD_") && pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) { 59 | return createOrFocusPopup(frameName, features); 60 | } 61 | 62 | if (url === "about:blank") return { action: "allow" }; 63 | 64 | // Drop the static temp page Discord web loads for the connections popout 65 | if (frameName === "authorize" && searchParams.get("loading") === "true") return { action: "deny" }; 66 | 67 | return handleExternalUrl(url, protocol); 68 | }); 69 | 70 | win.webContents.on("did-create-window", (win, { frameName }) => { 71 | if (frameName.startsWith("DISCORD_")) setupPopout(win, frameName); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/main/arrpc/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { resolve } from "path"; 8 | import { IpcCommands } from "shared/IpcEvents"; 9 | import { MessageChannel, Worker } from "worker_threads"; 10 | 11 | import { sendRendererCommand } from "../ipcCommands"; 12 | import { Settings } from "../settings"; 13 | import { ArRpcEvent, ArRpcHostEvent } from "./types"; 14 | 15 | let worker: Worker; 16 | 17 | const inviteCodeRegex = /^(\w|-)+$/; 18 | 19 | export async function initArRPC() { 20 | if (worker || !Settings.store.arRPC) return; 21 | 22 | try { 23 | const { port1: hostPort, port2: workerPort } = new MessageChannel(); 24 | 25 | worker = new Worker(resolve(__dirname, "./arRpcWorker.js"), { 26 | workerData: { 27 | workerPort 28 | }, 29 | transferList: [workerPort] 30 | }); 31 | 32 | hostPort.on("message", async ({ type, nonce, data }: ArRpcEvent) => { 33 | switch (type) { 34 | case "activity": { 35 | sendRendererCommand(IpcCommands.RPC_ACTIVITY, data); 36 | break; 37 | } 38 | 39 | case "invite": { 40 | const invite = String(data); 41 | 42 | const response: ArRpcHostEvent = { 43 | type: "ack-invite", 44 | nonce, 45 | data: false 46 | }; 47 | 48 | if (!inviteCodeRegex.test(invite)) { 49 | return hostPort.postMessage(response); 50 | } 51 | 52 | response.data = await sendRendererCommand(IpcCommands.RPC_INVITE, invite).catch(() => false); 53 | 54 | hostPort.postMessage(response); 55 | break; 56 | } 57 | 58 | case "link": { 59 | const response: ArRpcHostEvent = { 60 | type: "ack-link", 61 | nonce: nonce, 62 | data: false 63 | }; 64 | 65 | response.data = await sendRendererCommand(IpcCommands.RPC_DEEP_LINK, data).catch(() => false); 66 | 67 | hostPort.postMessage(response); 68 | break; 69 | } 70 | } 71 | }); 72 | } catch (e) { 73 | console.error("Failed to start arRPC server", e); 74 | } 75 | } 76 | 77 | Settings.addChangeListener("arRPC", initArRPC); 78 | -------------------------------------------------------------------------------- /src/main/utils/vencordLoader.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { mkdirSync } from "fs"; 8 | import { access, constants as FsConstants, writeFile } from "fs/promises"; 9 | import { VENCORD_FILES_DIR } from "main/vencordFilesDir"; 10 | import { join } from "path"; 11 | 12 | import { USER_AGENT } from "../constants"; 13 | import { downloadFile, fetchie } from "./http"; 14 | 15 | const API_BASE = "https://api.github.com"; 16 | 17 | export const FILES_TO_DOWNLOAD = [ 18 | "vencordDesktopMain.js", 19 | "vencordDesktopPreload.js", 20 | "vencordDesktopRenderer.js", 21 | "vencordDesktopRenderer.css" 22 | ]; 23 | 24 | export interface ReleaseData { 25 | name: string; 26 | tag_name: string; 27 | html_url: string; 28 | assets: Array<{ 29 | name: string; 30 | browser_download_url: string; 31 | }>; 32 | } 33 | 34 | export async function githubGet(endpoint: string) { 35 | const opts: RequestInit = { 36 | headers: { 37 | Accept: "application/vnd.github+json", 38 | "User-Agent": USER_AGENT 39 | } 40 | }; 41 | 42 | if (process.env.GITHUB_TOKEN) (opts.headers! as any).Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; 43 | 44 | return fetchie(API_BASE + endpoint, opts, { retryOnNetworkError: true }); 45 | } 46 | 47 | export async function downloadVencordFiles() { 48 | const release = await githubGet("/repos/Vendicated/Vencord/releases/latest"); 49 | 50 | const { assets }: ReleaseData = await release.json(); 51 | 52 | await Promise.all( 53 | assets 54 | .filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f))) 55 | .map(({ name, browser_download_url }) => 56 | downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name), {}, { retryOnNetworkError: true }) 57 | ) 58 | ); 59 | } 60 | 61 | const existsAsync = (path: string) => 62 | access(path, FsConstants.F_OK) 63 | .then(() => true) 64 | .catch(() => false); 65 | 66 | export async function isValidVencordInstall(dir: string) { 67 | const results = await Promise.all(["package.json", ...FILES_TO_DOWNLOAD].map(f => existsAsync(join(dir, f)))); 68 | return !results.includes(false); 69 | } 70 | 71 | export async function ensureVencordFiles() { 72 | if (await isValidVencordInstall(VENCORD_FILES_DIR)) return; 73 | 74 | mkdirSync(VENCORD_FILES_DIR, { recursive: true }); 75 | 76 | await Promise.all([downloadVencordFiles(), writeFile(join(VENCORD_FILES_DIR, "package.json"), "{}")]); 77 | } 78 | -------------------------------------------------------------------------------- /static/views/updater/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | margin: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | padding: 2em; 10 | display: flex; 11 | flex-direction: column; 12 | height: 100vh; 13 | 14 | header, 15 | footer { 16 | flex: 0 0 auto; 17 | } 18 | 19 | main { 20 | flex: 1 1 0%; 21 | min-height: 0; 22 | overflow: auto; 23 | } 24 | 25 | footer { 26 | margin-top: 1em; 27 | } 28 | } 29 | 30 | 31 | #versions { 32 | display: inline-grid; 33 | grid-template-columns: auto 1fr; 34 | column-gap: 0.5em; 35 | 36 | .label { 37 | white-space: nowrap; 38 | } 39 | 40 | .value { 41 | text-align: left; 42 | } 43 | 44 | #current-version { 45 | color: #fc8f8a; 46 | } 47 | 48 | #new-version { 49 | color: #71c07f; 50 | } 51 | } 52 | 53 | #release-notes { 54 | display: grid; 55 | gap: 1em; 56 | } 57 | 58 | #buttons { 59 | display: flex; 60 | gap: 0.5em; 61 | justify-content: end; 62 | } 63 | 64 | button { 65 | cursor: pointer; 66 | padding: 0.5rem 1rem; 67 | font-size: 1.2em; 68 | color: var(--fg); 69 | border: none; 70 | border-radius: 3px; 71 | font-weight: bold; 72 | transition: filter 0.2 ease-in-out; 73 | } 74 | 75 | button:hover, 76 | button:active { 77 | filter: brightness(0.9); 78 | } 79 | 80 | .green { 81 | background-color: #248046; 82 | } 83 | 84 | .grey { 85 | background-color: rgba(151, 151, 159, 0.12); 86 | } 87 | 88 | .hidden { 89 | display: none; 90 | } 91 | 92 | h1, 93 | h2, 94 | h3, 95 | h4, 96 | h5, 97 | h6 { 98 | margin: 0 0 0.5em 0; 99 | } 100 | 101 | h1 { 102 | text-align: center; 103 | } 104 | 105 | h2 { 106 | margin-bottom: 1em; 107 | } 108 | 109 | dialog { 110 | width: 80%; 111 | padding: 2em; 112 | } 113 | 114 | progress { 115 | width: 100%; 116 | height: 1.5em; 117 | margin-top: 1em; 118 | } 119 | 120 | #error { 121 | color: red; 122 | font-weight: bold; 123 | } 124 | 125 | .spinner-wrapper { 126 | display: flex; 127 | justify-content: center; 128 | align-items: center; 129 | margin-top: 2em; 130 | } 131 | 132 | .spinner { 133 | width: 48px; 134 | height: 48px; 135 | border: 5px solid var(--fg); 136 | border-bottom-color: transparent; 137 | border-radius: 50%; 138 | display: inline-block; 139 | box-sizing: border-box; 140 | animation: rotation 1s linear infinite; 141 | } 142 | 143 | @keyframes rotation { 144 | to { 145 | transform: rotate(360deg); 146 | } 147 | } -------------------------------------------------------------------------------- /src/main/tray.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { app, BrowserWindow, Menu, Tray } from "electron"; 8 | 9 | import { createAboutWindow } from "./about"; 10 | import { AppEvents } from "./events"; 11 | import { Settings } from "./settings"; 12 | import { resolveAssetPath } from "./userAssets"; 13 | import { clearData } from "./utils/clearData"; 14 | import { downloadVencordFiles } from "./utils/vencordLoader"; 15 | 16 | let tray: Tray; 17 | let trayVariant: "tray" | "trayUnread" = "tray"; 18 | 19 | AppEvents.on("userAssetChanged", async asset => { 20 | if (tray && (asset === "tray" || asset === "trayUnread")) { 21 | tray.setImage(await resolveAssetPath(trayVariant)); 22 | } 23 | }); 24 | 25 | AppEvents.on("setTrayVariant", async variant => { 26 | if (trayVariant === variant) return; 27 | 28 | trayVariant = variant; 29 | if (!tray) return; 30 | 31 | tray.setImage(await resolveAssetPath(trayVariant)); 32 | }); 33 | 34 | export function destroyTray() { 35 | tray?.destroy(); 36 | } 37 | 38 | export async function initTray(win: BrowserWindow, setIsQuitting: (val: boolean) => void) { 39 | const onTrayClick = () => { 40 | if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide(); 41 | else win.show(); 42 | }; 43 | 44 | const trayMenu = Menu.buildFromTemplate([ 45 | { 46 | label: "Open", 47 | click() { 48 | win.show(); 49 | } 50 | }, 51 | { 52 | label: "About", 53 | click: createAboutWindow 54 | }, 55 | { 56 | label: "Repair Vencord", 57 | async click() { 58 | await downloadVencordFiles(); 59 | app.relaunch(); 60 | app.quit(); 61 | } 62 | }, 63 | { 64 | label: "Reset Vesktop", 65 | async click() { 66 | await clearData(win); 67 | } 68 | }, 69 | { 70 | type: "separator" 71 | }, 72 | { 73 | label: "Restart", 74 | click() { 75 | app.relaunch(); 76 | app.quit(); 77 | } 78 | }, 79 | { 80 | label: "Quit", 81 | click() { 82 | setIsQuitting(true); 83 | app.quit(); 84 | } 85 | } 86 | ]); 87 | 88 | tray = new Tray(await resolveAssetPath(trayVariant)); 89 | tray.setToolTip("Vesktop"); 90 | tray.setContextMenu(trayMenu); 91 | tray.on("click", onTrayClick); 92 | } 93 | -------------------------------------------------------------------------------- /src/main/firstLaunch.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { app } from "electron"; 8 | import { BrowserWindow } from "electron/main"; 9 | import { copyFileSync, mkdirSync, readdirSync } from "fs"; 10 | import { join } from "path"; 11 | import { SplashProps } from "shared/browserWinProperties"; 12 | 13 | import { autoStart } from "./autoStart"; 14 | import { DATA_DIR } from "./constants"; 15 | import { createWindows } from "./mainWindow"; 16 | import { Settings, State } from "./settings"; 17 | import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; 18 | import { loadView } from "./vesktopStatic"; 19 | 20 | interface Data { 21 | discordBranch: "stable" | "canary" | "ptb"; 22 | minimizeToTray?: "on"; 23 | autoStart?: "on"; 24 | importSettings?: "on"; 25 | richPresence?: "on"; 26 | } 27 | 28 | export function createFirstLaunchTour() { 29 | const win = new BrowserWindow({ 30 | ...SplashProps, 31 | transparent: false, 32 | frame: true, 33 | autoHideMenuBar: true, 34 | height: 550, 35 | width: 600 36 | }); 37 | 38 | makeLinksOpenExternally(win); 39 | 40 | loadView(win, "first-launch.html"); 41 | win.webContents.addListener("console-message", (_e, _l, msg) => { 42 | if (msg === "cancel") return app.exit(); 43 | 44 | if (!msg.startsWith("form:")) return; 45 | const data = JSON.parse(msg.slice(5)) as Data; 46 | 47 | State.store.firstLaunch = false; 48 | Settings.store.discordBranch = data.discordBranch; 49 | Settings.store.minimizeToTray = !!data.minimizeToTray; 50 | Settings.store.arRPC = !!data.richPresence; 51 | 52 | if (data.autoStart) autoStart.enable(); 53 | 54 | if (data.importSettings) { 55 | const from = join(app.getPath("userData"), "..", "Vencord", "settings"); 56 | const to = join(DATA_DIR, "settings"); 57 | try { 58 | const files = readdirSync(from); 59 | mkdirSync(to, { recursive: true }); 60 | 61 | for (const file of files) { 62 | copyFileSync(join(from, file), join(to, file)); 63 | } 64 | } catch (e) { 65 | if (e instanceof Error && "code" in e && e.code === "ENOENT") { 66 | console.log("No Vencord settings found to import."); 67 | } else { 68 | console.error("Failed to import Vencord settings:", e); 69 | } 70 | } 71 | } 72 | 73 | win.close(); 74 | 75 | createWindows(); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /static/views/updater/script.js: -------------------------------------------------------------------------------- 1 | const { update, version: currentVersion } = await VesktopUpdaterNative.getData(); 2 | 3 | document.getElementById("current-version").textContent = currentVersion; 4 | document.getElementById("new-version").textContent = update.version; 5 | document.getElementById("release-notes").innerHTML = update.releaseNotes 6 | .map( 7 | ({ version, note: html }) => ` 8 |

9 |

Version ${version}

10 |
${html.replace(/<\/?h([1-3])/g, (m, level) => m.replace(level, Number(level) + 3))}
11 |
12 | ` 13 | ) 14 | .join("\n"); 15 | 16 | document.querySelectorAll("a").forEach(a => { 17 | a.target = "_blank"; 18 | }); 19 | 20 | // remove useless headings 21 | document.querySelectorAll("h3, h4, h5, h6").forEach(h => { 22 | if (h.textContent.trim().toLowerCase() === "what's changed") { 23 | h.remove(); 24 | } 25 | }); 26 | 27 | /** @type {HTMLDialogElement} */ 28 | const updateDialog = document.getElementById("update-dialog"); 29 | /** @type {HTMLDialogElement} */ 30 | const installingDialog = document.getElementById("installing-dialog"); 31 | /** @type {HTMLProgressElement} */ 32 | const downloadProgress = document.getElementById("download-progress"); 33 | /** @type {HTMLElement} */ 34 | const errorText = document.getElementById("error"); 35 | 36 | document.getElementById("update-button").addEventListener("click", () => { 37 | downloadProgress.value = 0; 38 | errorText.textContent = ""; 39 | 40 | if (navigator.platform.startsWith("Linux")) { 41 | document.getElementById("linux-note").classList.remove("hidden"); 42 | } 43 | 44 | updateDialog.showModal(); 45 | 46 | VesktopUpdaterNative.installUpdate().then(() => { 47 | downloadProgress.value = 100; 48 | updateDialog.closedBy = "any"; 49 | 50 | installingDialog.showModal(); 51 | updateDialog.classList.add("hidden"); 52 | }); 53 | }); 54 | 55 | document.getElementById("later-button").addEventListener("click", () => VesktopUpdaterNative.snoozeUpdate()); 56 | document.getElementById("ignore-button").addEventListener("click", () => { 57 | const confirmed = confirm( 58 | "Are you sure you want to ignore this update? You will not be notified about this update again. Updates are important for security and stability." 59 | ); 60 | if (confirmed) VesktopUpdaterNative.ignoreUpdate(); 61 | }); 62 | 63 | VesktopUpdaterNative.onProgress(percent => (downloadProgress.value = percent)); 64 | VesktopUpdaterNative.onError(message => { 65 | updateDialog.closedBy = "any"; 66 | errorText.textContent = `An error occurred while downloading the update: ${message}`; 67 | installingDialog.close(); 68 | updateDialog.classList.remove("hidden"); 69 | }); 70 | -------------------------------------------------------------------------------- /src/renderer/patches/screenShareFixes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { Logger } from "@vencord/types/utils"; 8 | import { currentSettings } from "renderer/components/ScreenSharePicker"; 9 | import { State } from "renderer/settings"; 10 | import { isLinux } from "renderer/utils"; 11 | 12 | const logger = new Logger("VesktopStreamFixes"); 13 | 14 | if (isLinux) { 15 | const original = navigator.mediaDevices.getDisplayMedia; 16 | 17 | async function getVirtmic() { 18 | try { 19 | const devices = await navigator.mediaDevices.enumerateDevices(); 20 | const audioDevice = devices.find(({ label }) => label === "vencord-screen-share"); 21 | return audioDevice?.deviceId; 22 | } catch (error) { 23 | return null; 24 | } 25 | } 26 | 27 | navigator.mediaDevices.getDisplayMedia = async function (opts) { 28 | const stream = await original.call(this, opts); 29 | const id = await getVirtmic(); 30 | 31 | const frameRate = Number(State.store.screenshareQuality?.frameRate ?? 30); 32 | const height = Number(State.store.screenshareQuality?.resolution ?? 720); 33 | const width = Math.round(height * (16 / 9)); 34 | const track = stream.getVideoTracks()[0]; 35 | 36 | track.contentHint = String(currentSettings?.contentHint); 37 | 38 | const constraints = { 39 | ...track.getConstraints(), 40 | frameRate: { min: frameRate, ideal: frameRate }, 41 | width: { min: 640, ideal: width, max: width }, 42 | height: { min: 480, ideal: height, max: height }, 43 | advanced: [{ width: width, height: height }], 44 | resizeMode: "none" 45 | }; 46 | 47 | track 48 | .applyConstraints(constraints) 49 | .then(() => { 50 | logger.info("Applied constraints successfully. New constraints: ", track.getConstraints()); 51 | }) 52 | .catch(e => logger.error("Failed to apply constraints.", e)); 53 | 54 | if (id) { 55 | const audio = await navigator.mediaDevices.getUserMedia({ 56 | audio: { 57 | deviceId: { 58 | exact: id 59 | }, 60 | autoGainControl: false, 61 | echoCancellation: false, 62 | noiseSuppression: false, 63 | channelCount: 2, 64 | sampleRate: 48000, 65 | sampleSize: 16 66 | } 67 | }); 68 | 69 | stream.getAudioTracks().forEach(t => stream.removeTrack(t)); 70 | stream.addTrack(audio.getAudioTracks()[0]); 71 | } 72 | 73 | return stream; 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/renderer/themedSplash.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { Settings } from "./settings"; 8 | 9 | function isValidColor(color: CSSStyleValue | undefined): color is CSSUnparsedValue & { [0]: string } { 10 | return color instanceof CSSUnparsedValue && typeof color[0] === "string" && CSS.supports("color", color[0]); 11 | } 12 | 13 | // https://gist.github.com/earthbound19/e7fe15fdf8ca3ef814750a61bc75b5ce 14 | function clamp(value: number, min: number, max: number) { 15 | return Math.max(Math.min(value, max), min); 16 | } 17 | const linearToGamma = (c: number) => (c >= 0.0031308 ? 1.055 * Math.pow(c, 1 / 2.4) - 0.055 : 12.92 * c); 18 | 19 | function oklabToSRGB({ L, a, b }: { L: number; a: number; b: number }) { 20 | let l = L + a * +0.3963377774 + b * +0.2158037573; 21 | let m = L + a * -0.1055613458 + b * -0.0638541728; 22 | let s = L + a * -0.0894841775 + b * -1.291485548; 23 | l **= 3; 24 | m **= 3; 25 | s **= 3; 26 | let R = l * +4.0767416621 + m * -3.3077115913 + s * +0.2309699292; 27 | let G = l * -1.2684380046 + m * +2.6097574011 + s * -0.3413193965; 28 | let B = l * -0.0041960863 + m * -0.7034186147 + s * +1.707614701; 29 | R = 255 * linearToGamma(R); 30 | G = 255 * linearToGamma(G); 31 | B = 255 * linearToGamma(B); 32 | R = Math.round(clamp(R, 0, 255)); 33 | G = Math.round(clamp(G, 0, 255)); 34 | B = Math.round(clamp(B, 0, 255)); 35 | 36 | return `rgb(${R}, ${G}, ${B})`; 37 | } 38 | 39 | function resolveColor(color: string) { 40 | const span = document.createElement("span"); 41 | span.style.color = color; 42 | span.style.display = "none"; 43 | 44 | document.body.append(span); 45 | let rgbColor = getComputedStyle(span).color; 46 | span.remove(); 47 | 48 | if (rgbColor.startsWith("oklab(")) { 49 | // scam 50 | const [_, L, a, b] = rgbColor.match(/oklab\((.+?)[, ]+(.+?)[, ]+(.+?)\)/) ?? []; 51 | if (L && a && b) { 52 | rgbColor = oklabToSRGB({ L: parseFloat(L), a: parseFloat(a), b: parseFloat(b) }); 53 | } 54 | } 55 | 56 | return rgbColor; 57 | } 58 | 59 | const updateSplashColors = () => { 60 | const bodyStyles = document.body.computedStyleMap(); 61 | 62 | const color = bodyStyles.get("--text-default"); 63 | const backgroundColor = bodyStyles.get("--background-base-lowest"); 64 | 65 | if (isValidColor(color)) { 66 | Settings.store.splashColor = resolveColor(color[0]); 67 | } 68 | 69 | if (isValidColor(backgroundColor)) { 70 | Settings.store.splashBackground = resolveColor(backgroundColor[0]); 71 | } 72 | }; 73 | 74 | if (document.readyState === "complete") { 75 | updateSplashColors(); 76 | } else { 77 | window.addEventListener("load", updateSplashColors); 78 | } 79 | 80 | window.addEventListener("beforeunload", updateSplashColors); 81 | -------------------------------------------------------------------------------- /src/shared/IpcEvents.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | export const enum IpcEvents { 8 | GET_VENCORD_PRELOAD_FILE = "VCD_GET_VC_PRELOAD_FILE", 9 | GET_VENCORD_RENDERER_SCRIPT = "VCD_GET_VC_RENDERER_SCRIPT", 10 | GET_RENDERER_SCRIPT = "VCD_GET_RENDERER_SCRIPT", 11 | GET_RENDERER_CSS_FILE = "VCD_GET_RENDERER_CSS_FILE", 12 | 13 | GET_VERSION = "VCD_GET_VERSION", 14 | SUPPORTS_WINDOWS_TRANSPARENCY = "VCD_SUPPORTS_WINDOWS_TRANSPARENCY", 15 | GET_ENABLE_HARDWARE_ACCELERATION = "VCD_GET_ENABLE_HARDWARE_ACCELERATION", 16 | 17 | RELAUNCH = "VCD_RELAUNCH", 18 | CLOSE = "VCD_CLOSE", 19 | FOCUS = "VCD_FOCUS", 20 | MINIMIZE = "VCD_MINIMIZE", 21 | MAXIMIZE = "VCD_MAXIMIZE", 22 | 23 | SHOW_ITEM_IN_FOLDER = "VCD_SHOW_ITEM_IN_FOLDER", 24 | GET_SETTINGS = "VCD_GET_SETTINGS", 25 | SET_SETTINGS = "VCD_SET_SETTINGS", 26 | 27 | GET_VENCORD_DIR = "VCD_GET_VENCORD_DIR", 28 | SELECT_VENCORD_DIR = "VCD_SELECT_VENCORD_DIR", 29 | 30 | UPDATER_IS_OUTDATED = "VCD_UPDATER_IS_OUTDATED", 31 | UPDATER_OPEN = "VCD_UPDATER_OPEN", 32 | 33 | SPELLCHECK_GET_AVAILABLE_LANGUAGES = "VCD_SPELLCHECK_GET_AVAILABLE_LANGUAGES", 34 | SPELLCHECK_RESULT = "VCD_SPELLCHECK_RESULT", 35 | SPELLCHECK_REPLACE_MISSPELLING = "VCD_SPELLCHECK_REPLACE_MISSPELLING", 36 | SPELLCHECK_ADD_TO_DICTIONARY = "VCD_SPELLCHECK_ADD_TO_DICTIONARY", 37 | 38 | SET_BADGE_COUNT = "VCD_SET_BADGE_COUNT", 39 | FLASH_FRAME = "FLASH_FRAME", 40 | 41 | CAPTURER_GET_LARGE_THUMBNAIL = "VCD_CAPTURER_GET_LARGE_THUMBNAIL", 42 | 43 | AUTOSTART_ENABLED = "VCD_AUTOSTART_ENABLED", 44 | ENABLE_AUTOSTART = "VCD_ENABLE_AUTOSTART", 45 | DISABLE_AUTOSTART = "VCD_DISABLE_AUTOSTART", 46 | 47 | VIRT_MIC_LIST = "VCD_VIRT_MIC_LIST", 48 | VIRT_MIC_START = "VCD_VIRT_MIC_START", 49 | VIRT_MIC_START_SYSTEM = "VCD_VIRT_MIC_START_ALL", 50 | VIRT_MIC_STOP = "VCD_VIRT_MIC_STOP", 51 | 52 | CLIPBOARD_COPY_IMAGE = "VCD_CLIPBOARD_COPY_IMAGE", 53 | 54 | DEBUG_LAUNCH_GPU = "VCD_DEBUG_LAUNCH_GPU", 55 | DEBUG_LAUNCH_WEBRTC_INTERNALS = "VCD_DEBUG_LAUNCH_WEBRTC", 56 | 57 | IPC_COMMAND = "VCD_IPC_COMMAND", 58 | 59 | DEVTOOLS_OPENED = "VCD_DEVTOOLS_OPENED", 60 | DEVTOOLS_CLOSED = "VCD_DEVTOOLS_CLOSED", 61 | 62 | CHOOSE_USER_ASSET = "VCD_CHOOSE_USER_ASSET" 63 | } 64 | 65 | export const enum UpdaterIpcEvents { 66 | GET_DATA = "VCD_UPDATER_GET_DATA", 67 | INSTALL = "VCD_UPDATER_INSTALL", 68 | DOWNLOAD_PROGRESS = "VCD_UPDATER_DOWNLOAD_PROGRESS", 69 | ERROR = "VCD_UPDATER_ERROR", 70 | SNOOZE_UPDATE = "VCD_UPDATER_SNOOZE_UPDATE", 71 | IGNORE_UPDATE = "VCD_UPDATER_IGNORE_UPDATE" 72 | } 73 | 74 | export const enum IpcCommands { 75 | RPC_ACTIVITY = "rpc:activity", 76 | RPC_INVITE = "rpc:invite", 77 | RPC_DEEP_LINK = "rpc:link", 78 | 79 | NAVIGATE_SETTINGS = "navigate:settings", 80 | 81 | GET_LANGUAGES = "navigator.languages", 82 | 83 | SCREEN_SHARE_PICKER = "screenshare:picker" 84 | } 85 | -------------------------------------------------------------------------------- /src/main/screenShare.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { desktopCapturer, session, Streams } from "electron"; 8 | import type { StreamPick } from "renderer/components/ScreenSharePicker"; 9 | import { IpcCommands, IpcEvents } from "shared/IpcEvents"; 10 | 11 | import { sendRendererCommand } from "./ipcCommands"; 12 | import { handle } from "./utils/ipcWrappers"; 13 | 14 | const isWayland = 15 | process.platform === "linux" && (process.env.XDG_SESSION_TYPE === "wayland" || !!process.env.WAYLAND_DISPLAY); 16 | 17 | export function registerScreenShareHandler() { 18 | handle(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, async (_, id: string) => { 19 | const sources = await desktopCapturer.getSources({ 20 | types: ["window", "screen"], 21 | thumbnailSize: { 22 | width: 1920, 23 | height: 1080 24 | } 25 | }); 26 | return sources.find(s => s.id === id)?.thumbnail.toDataURL(); 27 | }); 28 | 29 | session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => { 30 | // request full resolution on wayland right away because we always only end up with one result anyway 31 | const width = isWayland ? 1920 : 176; 32 | const sources = await desktopCapturer 33 | .getSources({ 34 | types: ["window", "screen"], 35 | thumbnailSize: { 36 | width, 37 | height: width * (9 / 16) 38 | } 39 | }) 40 | .catch(err => console.error("Error during screenshare picker", err)); 41 | 42 | if (!sources) return callback({}); 43 | 44 | const data = sources.map(({ id, name, thumbnail }) => ({ 45 | id, 46 | name, 47 | url: thumbnail.toDataURL() 48 | })); 49 | 50 | if (isWayland) { 51 | const video = data[0]; 52 | if (video) { 53 | const stream = await sendRendererCommand(IpcCommands.SCREEN_SHARE_PICKER, { 54 | screens: [video], 55 | skipPicker: true 56 | }).catch(() => null); 57 | 58 | if (stream === null) return callback({}); 59 | } 60 | 61 | callback(video ? { video: sources[0] } : {}); 62 | return; 63 | } 64 | 65 | const choice = await sendRendererCommand(IpcCommands.SCREEN_SHARE_PICKER, { 66 | screens: data, 67 | skipPicker: false 68 | }).catch(e => { 69 | console.error("Error during screenshare picker", e); 70 | return null; 71 | }); 72 | 73 | if (!choice) return callback({}); 74 | 75 | const source = sources.find(s => s.id === choice.id); 76 | if (!source) return callback({}); 77 | 78 | const streams: Streams = { 79 | video: source 80 | }; 81 | if (choice.audio && process.platform === "win32") streams.audio = "loopback"; 82 | 83 | callback(streams); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/main/updater.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { app, BrowserWindow, ipcMain } from "electron"; 8 | import { autoUpdater, UpdateInfo } from "electron-updater"; 9 | import { join } from "path"; 10 | import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents"; 11 | import { Millis } from "shared/utils/millis"; 12 | 13 | import { State } from "./settings"; 14 | import { handle } from "./utils/ipcWrappers"; 15 | import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; 16 | import { loadView } from "./vesktopStatic"; 17 | 18 | let updaterWindow: BrowserWindow | null = null; 19 | 20 | autoUpdater.on("update-available", update => { 21 | if (State.store.updater?.ignoredVersion === update.version) return; 22 | if ((State.store.updater?.snoozeUntil ?? 0) > Date.now()) return; 23 | 24 | openUpdater(update); 25 | }); 26 | 27 | autoUpdater.on("update-downloaded", () => setTimeout(() => autoUpdater.quitAndInstall(), 100)); 28 | autoUpdater.on("download-progress", p => 29 | updaterWindow?.webContents.send(UpdaterIpcEvents.DOWNLOAD_PROGRESS, p.percent) 30 | ); 31 | autoUpdater.on("error", err => updaterWindow?.webContents.send(UpdaterIpcEvents.ERROR, err.message)); 32 | 33 | autoUpdater.autoDownload = false; 34 | autoUpdater.autoInstallOnAppQuit = false; 35 | autoUpdater.fullChangelog = true; 36 | 37 | const isOutdated = autoUpdater.checkForUpdates().then(res => Boolean(res?.isUpdateAvailable)); 38 | 39 | handle(IpcEvents.UPDATER_IS_OUTDATED, () => isOutdated); 40 | handle(IpcEvents.UPDATER_OPEN, async () => { 41 | const res = await autoUpdater.checkForUpdates(); 42 | if (res?.isUpdateAvailable && res.updateInfo) openUpdater(res.updateInfo); 43 | }); 44 | 45 | function openUpdater(update: UpdateInfo) { 46 | updaterWindow = new BrowserWindow({ 47 | title: "Vesktop Updater", 48 | autoHideMenuBar: true, 49 | webPreferences: { 50 | preload: join(__dirname, "updaterPreload.js") 51 | }, 52 | minHeight: 400, 53 | minWidth: 750 54 | }); 55 | makeLinksOpenExternally(updaterWindow); 56 | 57 | handle(UpdaterIpcEvents.GET_DATA, () => ({ update, version: app.getVersion() })); 58 | handle(UpdaterIpcEvents.INSTALL, async () => { 59 | await autoUpdater.downloadUpdate(); 60 | }); 61 | handle(UpdaterIpcEvents.SNOOZE_UPDATE, () => { 62 | State.store.updater ??= {}; 63 | State.store.updater.snoozeUntil = Date.now() + 1 * Millis.DAY; 64 | updaterWindow?.close(); 65 | }); 66 | handle(UpdaterIpcEvents.IGNORE_UPDATE, () => { 67 | State.store.updater ??= {}; 68 | State.store.updater.ignoredVersion = update.version; 69 | updaterWindow?.close(); 70 | }); 71 | 72 | updaterWindow.on("closed", () => { 73 | ipcMain.removeHandler(UpdaterIpcEvents.GET_DATA); 74 | ipcMain.removeHandler(UpdaterIpcEvents.INSTALL); 75 | ipcMain.removeHandler(UpdaterIpcEvents.SNOOZE_UPDATE); 76 | ipcMain.removeHandler(UpdaterIpcEvents.IGNORE_UPDATE); 77 | updaterWindow = null; 78 | }); 79 | 80 | loadView(updaterWindow, "updater/index.html"); 81 | } 82 | -------------------------------------------------------------------------------- /src/renderer/components/screenSharePicker.css: -------------------------------------------------------------------------------- 1 | .vcd-screen-picker-modal { 2 | padding: 1em; 3 | } 4 | 5 | .vcd-screen-picker-header-close-button { 6 | margin-left: auto; 7 | } 8 | 9 | .vcd-screen-picker-venmic-settings { 10 | display: grid; 11 | gap: 8px; 12 | } 13 | 14 | .vcd-screen-picker-footer { 15 | display: flex; 16 | gap: 1em; 17 | } 18 | 19 | .vcd-screen-picker-card { 20 | flex-grow: 1; 21 | } 22 | 23 | 24 | /* Screen Grid */ 25 | .vcd-screen-picker-screen-grid { 26 | display: grid; 27 | grid-template-columns: 1fr 1fr; 28 | gap: 2em 1em; 29 | } 30 | 31 | .vcd-screen-picker-screen-radio { 32 | appearance: none; 33 | cursor: pointer; 34 | } 35 | 36 | .vcd-screen-picker-screen-label { 37 | overflow: hidden; 38 | padding: 8px; 39 | cursor: pointer; 40 | display: grid; 41 | justify-items: center; 42 | } 43 | 44 | .vcd-screen-picker-screen-label:hover { 45 | outline: 2px solid var(--brand-500); 46 | } 47 | 48 | .vcd-screen-picker-screen-name { 49 | white-space: nowrap; 50 | text-overflow: ellipsis; 51 | overflow: hidden; 52 | text-align: center; 53 | font-weight: 600; 54 | margin-inline: 0.5em; 55 | } 56 | 57 | .vcd-screen-picker-card { 58 | padding: 0.5em; 59 | box-sizing: border-box; 60 | } 61 | 62 | .vcd-screen-picker-preview-img-linux { 63 | width: 60%; 64 | margin-bottom: 0.5em; 65 | } 66 | 67 | .vcd-screen-picker-preview-img { 68 | width: 90%; 69 | margin-bottom: 0.5em; 70 | } 71 | 72 | .vcd-screen-picker-preview { 73 | display: flex; 74 | flex-direction: column; 75 | justify-content: center; 76 | align-items: center; 77 | margin-bottom: 1em; 78 | } 79 | 80 | 81 | /* Option Radios */ 82 | 83 | .vcd-screen-picker-option-radios { 84 | display: grid; 85 | grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); 86 | width: 100%; 87 | border-radius: 3px; 88 | } 89 | 90 | .vcd-screen-picker-option-radio { 91 | text-align: center; 92 | background-color: var(--background-secondary); 93 | border: 1px solid var(--primary-800); 94 | padding: 0.3em; 95 | cursor: pointer; 96 | } 97 | 98 | .vcd-screen-picker-option-radio:first-child { 99 | border-radius: 3px 0 0 3px; 100 | } 101 | 102 | .vcd-screen-picker-option-radio:last-child { 103 | border-radius: 0 3px 3px 0; 104 | } 105 | 106 | .vcd-screen-picker-option-input { 107 | display: none; 108 | } 109 | 110 | .vcd-screen-picker-option-radio[data-checked="true"] { 111 | background-color: var(--brand-500); 112 | border-color: var(--brand-500); 113 | } 114 | 115 | .vcd-screen-picker-quality { 116 | display: flex; 117 | gap: 1em; 118 | margin-bottom: 0.5em; 119 | } 120 | 121 | .vcd-screen-picker-quality-section { 122 | flex: 1 1 auto; 123 | } 124 | 125 | .vcd-screen-picker-settings-buttons { 126 | display: flex; 127 | justify-content: end; 128 | gap: 0.5em; 129 | 130 | margin-top: 0.75em; 131 | } 132 | 133 | .vcd-screen-picker-settings-button { 134 | display: flex; 135 | gap: 0.25em; 136 | padding-inline: 0.5em 1em; 137 | } 138 | 139 | .vcd-screen-picker-settings-button-icon { 140 | height: 1em; 141 | } 142 | 143 | .vcd-screen-picker-audio { 144 | margin-bottom: 0; 145 | } 146 | 147 | .vcd-screen-picker-audio-sources { 148 | display: flex; 149 | gap: 1em; 150 | 151 | >section { 152 | flex: 1; 153 | } 154 | } -------------------------------------------------------------------------------- /src/main/utils/popout.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { BrowserWindow, BrowserWindowConstructorOptions } from "electron"; 8 | import { Settings } from "main/settings"; 9 | 10 | import { handleExternalUrl } from "./makeLinksOpenExternally"; 11 | 12 | const ALLOWED_FEATURES = new Set([ 13 | "width", 14 | "height", 15 | "left", 16 | "top", 17 | "resizable", 18 | "movable", 19 | "alwaysOnTop", 20 | "frame", 21 | "transparent", 22 | "hasShadow", 23 | "closable", 24 | "skipTaskbar", 25 | "backgroundColor", 26 | "menubar", 27 | "toolbar", 28 | "location", 29 | "directories", 30 | "titleBarStyle" 31 | ]); 32 | 33 | const MIN_POPOUT_WIDTH = 320; 34 | const MIN_POPOUT_HEIGHT = 180; 35 | const DEFAULT_POPOUT_OPTIONS: BrowserWindowConstructorOptions = { 36 | title: "Discord Popout", 37 | backgroundColor: "#2f3136", 38 | minWidth: MIN_POPOUT_WIDTH, 39 | minHeight: MIN_POPOUT_HEIGHT, 40 | frame: Settings.store.customTitleBar !== true, 41 | titleBarStyle: process.platform === "darwin" ? "hidden" : undefined, 42 | trafficLightPosition: 43 | process.platform === "darwin" 44 | ? { 45 | x: 10, 46 | y: 3 47 | } 48 | : undefined, 49 | webPreferences: { 50 | nodeIntegration: false, 51 | contextIsolation: true 52 | }, 53 | autoHideMenuBar: Settings.store.enableMenu 54 | }; 55 | 56 | export const PopoutWindows = new Map(); 57 | 58 | function focusWindow(window: BrowserWindow) { 59 | window.setAlwaysOnTop(true); 60 | window.focus(); 61 | window.setAlwaysOnTop(false); 62 | } 63 | 64 | function parseFeatureValue(feature: string) { 65 | if (feature === "yes") return true; 66 | if (feature === "no") return false; 67 | 68 | const n = Number(feature); 69 | if (!isNaN(n)) return n; 70 | 71 | return feature; 72 | } 73 | 74 | function parseWindowFeatures(features: string) { 75 | const keyValuesParsed = features.split(","); 76 | 77 | return keyValuesParsed.reduce((features, feature) => { 78 | const [key, value] = feature.split("="); 79 | if (ALLOWED_FEATURES.has(key)) features[key] = parseFeatureValue(value); 80 | 81 | return features; 82 | }, {}); 83 | } 84 | 85 | export function createOrFocusPopup(key: string, features: string) { 86 | const existingWindow = PopoutWindows.get(key); 87 | if (existingWindow) { 88 | focusWindow(existingWindow); 89 | return { action: "deny" }; 90 | } 91 | 92 | return { 93 | action: "allow", 94 | overrideBrowserWindowOptions: { 95 | ...DEFAULT_POPOUT_OPTIONS, 96 | ...parseWindowFeatures(features) 97 | } 98 | }; 99 | } 100 | 101 | export function setupPopout(win: BrowserWindow, key: string) { 102 | win.setMenuBarVisibility(false); 103 | 104 | PopoutWindows.set(key, win); 105 | 106 | /* win.webContents.on("will-navigate", (evt, url) => { 107 | // maybe prevent if not origin match 108 | })*/ 109 | 110 | win.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url)); 111 | 112 | win.once("closed", () => { 113 | win.removeAllListeners(); 114 | PopoutWindows.delete(key); 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /src/main/userAssets.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { app, dialog, net } from "electron"; 8 | import { copyFile, mkdir, rm } from "fs/promises"; 9 | import { join } from "path"; 10 | import { IpcEvents } from "shared/IpcEvents"; 11 | import { STATIC_DIR } from "shared/paths"; 12 | import { pathToFileURL } from "url"; 13 | 14 | import { DATA_DIR } from "./constants"; 15 | import { AppEvents } from "./events"; 16 | import { mainWin } from "./mainWindow"; 17 | import { fileExistsAsync } from "./utils/fileExists"; 18 | import { handle } from "./utils/ipcWrappers"; 19 | 20 | const CUSTOMIZABLE_ASSETS = ["splash", "tray", "trayUnread"] as const; 21 | export type UserAssetType = (typeof CUSTOMIZABLE_ASSETS)[number]; 22 | 23 | const DEFAULT_ASSETS: Record = { 24 | splash: "splash.webp", 25 | tray: `tray/${process.platform === "darwin" ? "trayTemplate" : "tray"}.png`, 26 | trayUnread: "tray/trayUnread.png" 27 | }; 28 | 29 | const UserAssetFolder = join(DATA_DIR, "userAssets"); 30 | 31 | export async function resolveAssetPath(asset: UserAssetType) { 32 | if (!CUSTOMIZABLE_ASSETS.includes(asset)) { 33 | throw new Error(`Invalid asset: ${asset}`); 34 | } 35 | 36 | const assetPath = join(UserAssetFolder, asset); 37 | if (await fileExistsAsync(assetPath)) { 38 | return assetPath; 39 | } 40 | 41 | return join(STATIC_DIR, DEFAULT_ASSETS[asset]); 42 | } 43 | 44 | export async function handleVesktopAssetsProtocol(path: string, req: Request) { 45 | const asset = path.slice(1); 46 | 47 | // @ts-expect-error dumb types 48 | if (!CUSTOMIZABLE_ASSETS.includes(asset)) { 49 | return new Response(null, { status: 404 }); 50 | } 51 | 52 | try { 53 | const res = await net.fetch(pathToFileURL(join(UserAssetFolder, asset)).href); 54 | if (res.ok) return res; 55 | } catch {} 56 | 57 | return net.fetch(pathToFileURL(join(STATIC_DIR, DEFAULT_ASSETS[asset])).href); 58 | } 59 | 60 | handle(IpcEvents.CHOOSE_USER_ASSET, async (_event, asset: UserAssetType, value?: null) => { 61 | if (!CUSTOMIZABLE_ASSETS.includes(asset)) { 62 | throw `Invalid asset: ${asset}`; 63 | } 64 | 65 | const assetPath = join(UserAssetFolder, asset); 66 | 67 | if (value === null) { 68 | try { 69 | await rm(assetPath, { force: true }); 70 | AppEvents.emit("userAssetChanged", asset); 71 | return "ok"; 72 | } catch (e) { 73 | console.error(`Failed to remove user asset ${asset}:`, e); 74 | return "failed"; 75 | } 76 | } 77 | 78 | const res = await dialog.showOpenDialog(mainWin, { 79 | properties: ["openFile"], 80 | title: `Select an image to use as ${asset}`, 81 | defaultPath: app.getPath("pictures"), 82 | filters: [ 83 | { 84 | name: "Images", 85 | extensions: ["png", "jpg", "jpeg", "webp", "gif", "avif", "svg"] 86 | } 87 | ] 88 | }); 89 | 90 | if (res.canceled || !res.filePaths.length) return "cancelled"; 91 | 92 | try { 93 | await mkdir(UserAssetFolder, { recursive: true }); 94 | await copyFile(res.filePaths[0], assetPath); 95 | AppEvents.emit("userAssetChanged", asset); 96 | return "ok"; 97 | } catch (e) { 98 | console.error(`Failed to copy user asset ${asset}:`, e); 99 | return "failed"; 100 | } 101 | }); 102 | -------------------------------------------------------------------------------- /src/main/autoStart.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { app } from "electron"; 8 | import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"; 9 | import { join } from "path"; 10 | import { stripIndent } from "shared/utils/text"; 11 | 12 | import { IS_FLATPAK } from "./constants"; 13 | import { requestBackground } from "./dbus"; 14 | import { Settings, State } from "./settings"; 15 | import { escapeDesktopFileArgument } from "./utils/desktopFileEscape"; 16 | 17 | interface AutoStart { 18 | isEnabled(): boolean; 19 | enable(): void; 20 | disable(): void; 21 | } 22 | 23 | function getEscapedCommandLine() { 24 | const args = process.argv.map(escapeDesktopFileArgument); 25 | if (Settings.store.autoStartMinimized) args.push("--start-minimized"); 26 | return args; 27 | } 28 | 29 | function makeAutoStartLinuxDesktop(): AutoStart { 30 | const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config"); 31 | const dir = join(configDir, "autostart"); 32 | const file = join(dir, "vesktop.desktop"); 33 | 34 | return { 35 | isEnabled: () => existsSync(file), 36 | enable() { 37 | const desktopFile = stripIndent` 38 | [Desktop Entry] 39 | Type=Application 40 | Name=Vesktop 41 | Comment=Vesktop autostart script 42 | Exec=${getEscapedCommandLine().join(" ")} 43 | StartupNotify=false 44 | Terminal=false 45 | Icon=vesktop 46 | `; 47 | 48 | mkdirSync(dir, { recursive: true }); 49 | writeFileSync(file, desktopFile); 50 | }, 51 | disable: () => rmSync(file, { force: true }) 52 | }; 53 | } 54 | 55 | function makeAutoStartLinuxPortal() { 56 | return { 57 | isEnabled: () => State.store.linuxAutoStartEnabled === true, 58 | enable() { 59 | const success = requestBackground(true, getEscapedCommandLine()); 60 | if (success) { 61 | State.store.linuxAutoStartEnabled = true; 62 | } 63 | return success; 64 | }, 65 | disable() { 66 | const success = requestBackground(false, []); 67 | if (success) { 68 | State.store.linuxAutoStartEnabled = false; 69 | } 70 | return success; 71 | } 72 | }; 73 | } 74 | 75 | const autoStartWindowsMac: AutoStart = { 76 | isEnabled: () => app.getLoginItemSettings().openAtLogin, 77 | enable: () => 78 | app.setLoginItemSettings({ 79 | openAtLogin: true, 80 | args: Settings.store.autoStartMinimized ? ["--start-minimized"] : [] 81 | }), 82 | disable: () => app.setLoginItemSettings({ openAtLogin: false }) 83 | }; 84 | 85 | // The portal call uses the app id by default, which is org.chromium.Chromium, even in packaged Vesktop. 86 | // This leads to an autostart entry named "Chromium" instead of "Vesktop". 87 | // Thus, only use the portal inside Flatpak, where the app is actually correct. 88 | // Maybe there is a way to fix it outside of flatpak, but I couldn't figure it out. 89 | export const autoStart = 90 | process.platform !== "linux" 91 | ? autoStartWindowsMac 92 | : IS_FLATPAK 93 | ? makeAutoStartLinuxPortal() 94 | : makeAutoStartLinuxDesktop(); 95 | 96 | Settings.addChangeListener("autoStartMinimized", () => { 97 | if (!autoStart.isEnabled()) return; 98 | 99 | autoStart.enable(); 100 | }); 101 | -------------------------------------------------------------------------------- /static/views/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | About Vesktop 6 | 7 | 8 | 9 | 24 | 25 | 26 | 27 |

Vesktop v{{APP_VERSION}}

28 |

Vesktop is a cross platform Discord Desktop client, aiming to give you a better Discord experience

29 | 30 |
31 |

Links

32 | 46 |
47 | 48 |
49 |

License

50 |

51 | Vesktop is licensed under the 52 | GNU General Public License v3.0. 53 |
54 | This is free software, and you are welcome to redistribute it under certain conditions; see the license for 55 | details. 56 |

57 |
58 | 59 |
60 |

Acknowledgements

61 |

These awesome libraries empower Vesktop

62 |
    63 |
  • 64 | Electron 65 | - Build cross-platform desktop apps with JavaScript, HTML, and CSS 66 |
  • 67 |
  • 68 | Electron Builder 69 | - A complete solution to package and build a ready for distribution Electron app with "auto update" 70 | support out of the box 71 |
  • 72 |
  • 73 | arrpc 74 | - An open implementation of Discord's Rich Presence server 75 |
  • 76 |
  • 77 | rohrkabel 78 | - A C++ RAII Pipewire-API Wrapper 79 |
  • 80 |
  • 81 | And many 82 | more open source 83 | libraries 84 |
  • 85 |
86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/utils/steamOS.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { BrowserWindow, dialog } from "electron"; 8 | import { writeFile } from "fs/promises"; 9 | import { join } from "path"; 10 | 11 | import { MessageBoxChoice } from "../constants"; 12 | import { State } from "../settings"; 13 | 14 | // Bump this to re-show the prompt 15 | const layoutVersion = 2; 16 | // Get this from "show details" on the profile after exporting as a shared personal layout or using share with community 17 | const layoutId = "3080264545"; // Vesktop Layout v2 18 | const numberRegex = /^[0-9]*$/; 19 | 20 | let steamPipeQueue = Promise.resolve(); 21 | 22 | export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1"; 23 | 24 | export function applyDeckKeyboardFix() { 25 | if (!isDeckGameMode) return; 26 | // Prevent constant virtual keyboard spam that eventually crashes Steam. 27 | process.env.GTK_IM_MODULE = "None"; 28 | } 29 | 30 | // For some reason SteamAppId is always 0 for non-steam apps so we do this insanity instead. 31 | function getAppId(): string | null { 32 | // /home/deck/.local/share/Steam/steamapps/shadercache/APPID/fozmediav1 33 | const path = process.env.STEAM_COMPAT_MEDIA_PATH; 34 | if (!path) return null; 35 | const pathElems = path?.split("/"); 36 | const appId = pathElems[pathElems.length - 2]; 37 | if (appId.match(numberRegex)) { 38 | console.log(`Got Steam App ID ${appId}`); 39 | return appId; 40 | } 41 | return null; 42 | } 43 | 44 | export function execSteamURL(url: string) { 45 | // This doesn't allow arbitrary execution despite the weird syntax. 46 | steamPipeQueue = steamPipeQueue.then(() => 47 | writeFile( 48 | join(process.env.HOME || "/home/deck", ".steam", "steam.pipe"), 49 | // replace ' to prevent argument injection 50 | `'${process.env.HOME}/.local/share/Steam/ubuntu12_32/steam' '-ifrunning' '${url.replaceAll("'", "%27")}'\n`, 51 | "utf-8" 52 | ) 53 | ); 54 | } 55 | 56 | export function steamOpenURL(url: string) { 57 | execSteamURL(`steam://openurl/${url}`); 58 | } 59 | 60 | export async function showGamePage() { 61 | const appId = getAppId(); 62 | if (!appId) return; 63 | await execSteamURL(`steam://nav/games/details/${appId}`); 64 | } 65 | 66 | async function showLayout(appId: string) { 67 | execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`); 68 | } 69 | 70 | export async function askToApplySteamLayout(win: BrowserWindow) { 71 | const appId = getAppId(); 72 | if (!appId) return; 73 | if (State.store.steamOSLayoutVersion === layoutVersion) return; 74 | const update = Boolean(State.store.steamOSLayoutVersion); 75 | 76 | // Touch screen breaks in some menus when native touch mode is enabled on latest SteamOS beta, remove most of the update specific text once that's fixed. 77 | const { response } = await dialog.showMessageBox(win, { 78 | message: `${update ? "Update" : "Apply"} Vesktop Steam Input Layout?`, 79 | detail: `Would you like to ${update ? "Update" : "Apply"} Vesktop's recommended Steam Deck controller settings? 80 | ${update ? "Click yes using the touchpad" : "Tap yes"}, then press the X button or tap Apply Layout to confirm.${ 81 | update ? " Doing so will undo any customizations you have made." : "" 82 | } 83 | ${update ? "Click" : "Tap"} no to keep your current layout.`, 84 | buttons: ["Yes", "No"], 85 | cancelId: MessageBoxChoice.Cancel, 86 | defaultId: MessageBoxChoice.Default, 87 | type: "question" 88 | }); 89 | 90 | if (State.store.steamOSLayoutVersion !== layoutVersion) { 91 | State.store.steamOSLayoutVersion = layoutVersion; 92 | } 93 | 94 | if (response === MessageBoxChoice.Cancel) return; 95 | 96 | await showLayout(appId); 97 | } 98 | -------------------------------------------------------------------------------- /src/renderer/components/settings/UserAssets.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import "./UserAssets.css"; 8 | 9 | import { BaseText, Button, FormSwitch } from "@vencord/types/components"; 10 | import { 11 | Margins, 12 | ModalCloseButton, 13 | ModalContent, 14 | ModalHeader, 15 | ModalRoot, 16 | ModalSize, 17 | openModal, 18 | wordsFromCamel, 19 | wordsToTitle 20 | } from "@vencord/types/utils"; 21 | import { showToast, useState } from "@vencord/types/webpack/common"; 22 | import { UserAssetType } from "main/userAssets"; 23 | import { useSettings } from "renderer/settings"; 24 | 25 | import { SettingsComponent } from "./Settings"; 26 | 27 | const CUSTOMIZABLE_ASSETS: UserAssetType[] = ["splash", "tray", "trayUnread"]; 28 | 29 | export const UserAssetsButton: SettingsComponent = () => { 30 | return ; 31 | }; 32 | 33 | function openAssetsModal() { 34 | openModal(props => ( 35 | 36 | 37 | 38 | User Assets 39 | 40 | 41 | 42 | 43 | 44 |
45 | {CUSTOMIZABLE_ASSETS.map(asset => ( 46 | 47 | ))} 48 |
49 |
50 |
51 | )); 52 | } 53 | 54 | function Asset({ asset }: { asset: UserAssetType }) { 55 | // cache busting 56 | const [version, setVersion] = useState(Date.now()); 57 | const settings = useSettings(); 58 | 59 | const isSplash = asset === "splash"; 60 | const imageRendering = isSplash && settings.splashPixelated ? "pixelated" : "auto"; 61 | 62 | const onChooseAsset = (value?: null) => async () => { 63 | const res = await VesktopNative.fileManager.chooseUserAsset(asset, value); 64 | if (res === "ok") { 65 | setVersion(Date.now()); 66 | if (isSplash && value === null) { 67 | settings.splashPixelated = false; 68 | } 69 | } else if (res === "failed") { 70 | showToast("Something went wrong. Please try again"); 71 | } 72 | }; 73 | 74 | return ( 75 |
76 | 77 | {wordsToTitle(wordsFromCamel(asset))} 78 | 79 |
80 | 86 |
87 |
88 | 89 | 92 |
93 | {isSplash && ( 94 | (settings.splashPixelated = val)} 98 | className={Margins.top16} 99 | hideBorder 100 | /> 101 | )} 102 |
103 |
104 |
105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /scripts/utils/generateMeta.mts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { promises as fs } from "node:fs"; 8 | import { mkdir } from "node:fs/promises"; 9 | import { DOMParser, XMLSerializer } from "@xmldom/xmldom"; 10 | import xmlFormat from "xml-formatter"; 11 | 12 | function generateDescription(description: string, descriptionNode: Element) { 13 | const lines = description.replace(/\r/g, "").split("\n"); 14 | let currentList: Element | null = null; 15 | 16 | for (let i = 0; i < lines.length; i++) { 17 | const line = lines[i]; 18 | 19 | if (line.includes("New Contributors")) { 20 | // we're done, don't parse any more since the new contributors section is the last one 21 | break; 22 | } 23 | 24 | if (line.startsWith("## ")) { 25 | const pNode = descriptionNode.ownerDocument.createElement("p"); 26 | pNode.textContent = line.slice(3); 27 | descriptionNode.appendChild(pNode); 28 | } else if (line.startsWith("* ")) { 29 | const liNode = descriptionNode.ownerDocument.createElement("li"); 30 | liNode.textContent = line.slice(2).split("in https://github.com")[0].trim(); // don't include links to github 31 | 32 | if (!currentList) { 33 | currentList = descriptionNode.ownerDocument.createElement("ul"); 34 | } 35 | 36 | currentList.appendChild(liNode); 37 | } 38 | 39 | if (currentList && !lines[i + 1].startsWith("* ")) { 40 | descriptionNode.appendChild(currentList); 41 | currentList = null; 42 | } 43 | } 44 | } 45 | 46 | const releases = await fetch("https://api.github.com/repos/Vencord/Vesktop/releases", { 47 | headers: { 48 | Accept: "application/vnd.github+json", 49 | "X-Github-Api-Version": "2022-11-28" 50 | } 51 | }).then(res => res.json()); 52 | 53 | const latestReleaseInformation = releases[0]; 54 | 55 | const metaInfo = await (async () => { 56 | for (const release of releases) { 57 | const metaAsset = release.assets.find((a: any) => a.name === "dev.vencord.Vesktop.metainfo.xml"); 58 | if (metaAsset) return fetch(metaAsset.browser_download_url).then(res => res.text()); 59 | } 60 | })(); 61 | 62 | if (!metaInfo) { 63 | throw new Error("Could not find existing meta information from any release"); 64 | } 65 | 66 | const parser = new DOMParser().parseFromString(metaInfo, "text/xml"); 67 | 68 | const releaseList = parser.getElementsByTagName("releases")[0]; 69 | 70 | for (let i = 0; i < releaseList.childNodes.length; i++) { 71 | const release = releaseList.childNodes[i] as Element; 72 | 73 | if (release.nodeType === 1 && release.getAttribute("version") === latestReleaseInformation.name) { 74 | console.log("Latest release already added, nothing to be done"); 75 | process.exit(0); 76 | } 77 | } 78 | 79 | const release = parser.createElement("release"); 80 | release.setAttribute("version", latestReleaseInformation.name); 81 | release.setAttribute("date", latestReleaseInformation.published_at.split("T")[0]); 82 | release.setAttribute("type", "stable"); 83 | 84 | const releaseUrl = parser.createElement("url"); 85 | releaseUrl.textContent = latestReleaseInformation.html_url; 86 | 87 | release.appendChild(releaseUrl); 88 | 89 | const description = parser.createElement("description"); 90 | 91 | // we're not using a full markdown parser here since we don't have a lot of formatting options to begin with 92 | generateDescription(latestReleaseInformation.body, description); 93 | 94 | release.appendChild(description); 95 | 96 | releaseList.insertBefore(release, releaseList.childNodes[0]); 97 | 98 | const output = xmlFormat(new XMLSerializer().serializeToString(parser), { 99 | lineSeparator: "\n", 100 | collapseContent: true, 101 | indentation: " " 102 | }); 103 | 104 | await mkdir("./dist", { recursive: true }); 105 | await fs.writeFile("./dist/dev.vencord.Vesktop.metainfo.xml", output, "utf-8"); 106 | 107 | console.log("Updated meta information written to ./dist/dev.vencord.Vesktop.metainfo.xml"); 108 | -------------------------------------------------------------------------------- /src/main/venmic.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import type { LinkData, Node, PatchBay as PatchBayType } from "@vencord/venmic"; 8 | import { app, ipcMain } from "electron"; 9 | import { join } from "path"; 10 | import { IpcEvents } from "shared/IpcEvents"; 11 | import { STATIC_DIR } from "shared/paths"; 12 | 13 | import { Settings } from "./settings"; 14 | 15 | let PatchBay: typeof PatchBayType | undefined; 16 | let patchBayInstance: PatchBayType | undefined; 17 | 18 | let imported = false; 19 | let initialized = false; 20 | 21 | let hasPipewirePulse = false; 22 | let isGlibCxxOutdated = false; 23 | 24 | function importVenmic() { 25 | if (imported) { 26 | return; 27 | } 28 | 29 | imported = true; 30 | 31 | try { 32 | PatchBay = (require(join(STATIC_DIR, `dist/venmic-${process.arch}.node`)) as typeof import("@vencord/venmic")) 33 | .PatchBay; 34 | 35 | hasPipewirePulse = PatchBay.hasPipeWire(); 36 | } catch (e: any) { 37 | console.error("Failed to import venmic", e); 38 | isGlibCxxOutdated = (e?.stack || e?.message || "").toLowerCase().includes("glibc"); 39 | } 40 | } 41 | 42 | function obtainVenmic() { 43 | if (!imported) { 44 | importVenmic(); 45 | } 46 | 47 | if (PatchBay && !initialized) { 48 | initialized = true; 49 | 50 | try { 51 | patchBayInstance = new PatchBay(); 52 | } catch (e: any) { 53 | console.error("Failed to instantiate venmic", e); 54 | } 55 | } 56 | 57 | return patchBayInstance; 58 | } 59 | 60 | function getRendererAudioServicePid() { 61 | return ( 62 | app 63 | .getAppMetrics() 64 | .find(proc => proc.name === "Audio Service") 65 | ?.pid?.toString() ?? "owo" 66 | ); 67 | } 68 | 69 | ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => { 70 | const audioPid = getRendererAudioServicePid(); 71 | 72 | const { granularSelect } = Settings.store.audio ?? {}; 73 | 74 | const targets = obtainVenmic() 75 | ?.list(granularSelect ? ["node.name"] : undefined) 76 | .filter(s => s["application.process.id"] !== audioPid); 77 | 78 | return targets ? { ok: true, targets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated }; 79 | }); 80 | 81 | ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, include: Node[]) => { 82 | const pid = getRendererAudioServicePid(); 83 | const { ignoreDevices, ignoreInputMedia, ignoreVirtual, workaround } = Settings.store.audio ?? {}; 84 | 85 | const data: LinkData = { 86 | include, 87 | exclude: [{ "application.process.id": pid }], 88 | ignore_devices: ignoreDevices 89 | }; 90 | 91 | if (ignoreInputMedia ?? true) { 92 | data.exclude.push({ "media.class": "Stream/Input/Audio" }); 93 | } 94 | 95 | if (ignoreVirtual) { 96 | data.exclude.push({ "node.virtual": "true" }); 97 | } 98 | 99 | if (workaround) { 100 | data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }]; 101 | } 102 | 103 | return obtainVenmic()?.link(data); 104 | }); 105 | 106 | ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, exclude: Node[]) => { 107 | const pid = getRendererAudioServicePid(); 108 | 109 | const { workaround, ignoreDevices, ignoreInputMedia, ignoreVirtual, onlySpeakers, onlyDefaultSpeakers } = 110 | Settings.store.audio ?? {}; 111 | 112 | const data: LinkData = { 113 | include: [], 114 | exclude: [{ "application.process.id": pid }, ...exclude], 115 | only_speakers: onlySpeakers, 116 | ignore_devices: ignoreDevices, 117 | only_default_speakers: onlyDefaultSpeakers 118 | }; 119 | 120 | if (ignoreInputMedia ?? true) { 121 | data.exclude.push({ "media.class": "Stream/Input/Audio" }); 122 | } 123 | 124 | if (ignoreVirtual) { 125 | data.exclude.push({ "node.virtual": "true" }); 126 | } 127 | 128 | if (workaround) { 129 | data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }]; 130 | } 131 | 132 | return obtainVenmic()?.link(data); 133 | }); 134 | 135 | ipcMain.handle(IpcEvents.VIRT_MIC_STOP, () => obtainVenmic()?.unlink()); 136 | -------------------------------------------------------------------------------- /scripts/build/build.mts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { BuildContext, BuildOptions, context } from "esbuild"; 8 | import { copyFile } from "fs/promises"; 9 | 10 | import vencordDep from "./vencordDep.mjs"; 11 | import { includeDirPlugin } from "./includeDirPlugin.mts"; 12 | 13 | const isDev = process.argv.includes("--dev"); 14 | 15 | const CommonOpts: BuildOptions = { 16 | minify: !isDev, 17 | bundle: true, 18 | sourcemap: "linked", 19 | logLevel: "info" 20 | }; 21 | 22 | const NodeCommonOpts: BuildOptions = { 23 | ...CommonOpts, 24 | format: "cjs", 25 | platform: "node", 26 | external: ["electron"], 27 | target: ["esnext"], 28 | loader: { 29 | ".node": "file" 30 | }, 31 | define: { 32 | IS_DEV: JSON.stringify(isDev) 33 | } 34 | }; 35 | 36 | const contexts = [] as BuildContext[]; 37 | async function createContext(options: BuildOptions) { 38 | contexts.push(await context(options)); 39 | } 40 | 41 | async function copyVenmic() { 42 | if (process.platform !== "linux") return; 43 | 44 | return Promise.all([ 45 | copyFile( 46 | "./node_modules/@vencord/venmic/prebuilds/venmic-addon-linux-x64/node-napi-v7.node", 47 | "./static/dist/venmic-x64.node" 48 | ), 49 | copyFile( 50 | "./node_modules/@vencord/venmic/prebuilds/venmic-addon-linux-arm64/node-napi-v7.node", 51 | "./static/dist/venmic-arm64.node" 52 | ) 53 | ]).catch(() => console.warn("Failed to copy venmic. Building without venmic support")); 54 | } 55 | 56 | async function copyLibVesktop() { 57 | if (process.platform !== "linux") return; 58 | 59 | try { 60 | await copyFile( 61 | "./packages/libvesktop/build/Release/vesktop.node", 62 | `./static/dist/libvesktop-${process.arch}.node` 63 | ); 64 | console.log("Using local libvesktop build"); 65 | } catch { 66 | console.log( 67 | "Using prebuilt libvesktop binaries. Run `pnpm buildLibVesktop` and build again to build from source - see README.md for more details" 68 | ); 69 | return Promise.all([ 70 | copyFile("./packages/libvesktop/prebuilds/vesktop-x64.node", "./static/dist/libvesktop-x64.node"), 71 | copyFile("./packages/libvesktop/prebuilds/vesktop-arm64.node", "./static/dist/libvesktop-arm64.node") 72 | ]).catch(() => console.warn("Failed to copy libvesktop. Building without libvesktop support")); 73 | } 74 | } 75 | 76 | await Promise.all([ 77 | copyVenmic(), 78 | copyLibVesktop(), 79 | createContext({ 80 | ...NodeCommonOpts, 81 | entryPoints: ["src/main/index.ts"], 82 | outfile: "dist/js/main.js", 83 | footer: { js: "//# sourceURL=VesktopMain" } 84 | }), 85 | createContext({ 86 | ...NodeCommonOpts, 87 | entryPoints: ["src/main/arrpc/worker.ts"], 88 | outfile: "dist/js/arRpcWorker.js", 89 | footer: { js: "//# sourceURL=VesktopArRpcWorker" } 90 | }), 91 | createContext({ 92 | ...NodeCommonOpts, 93 | entryPoints: ["src/preload/index.ts"], 94 | outfile: "dist/js/preload.js", 95 | footer: { js: "//# sourceURL=VesktopPreload" } 96 | }), 97 | createContext({ 98 | ...NodeCommonOpts, 99 | entryPoints: ["src/preload/splash.ts"], 100 | outfile: "dist/js/splashPreload.js", 101 | footer: { js: "//# sourceURL=VesktopSplashPreload" } 102 | }), 103 | createContext({ 104 | ...NodeCommonOpts, 105 | entryPoints: ["src/preload/updater.ts"], 106 | outfile: "dist/js/updaterPreload.js", 107 | footer: { js: "//# sourceURL=VesktopUpdaterPreload" } 108 | }), 109 | createContext({ 110 | ...CommonOpts, 111 | globalName: "Vesktop", 112 | entryPoints: ["src/renderer/index.ts"], 113 | outfile: "dist/js/renderer.js", 114 | format: "iife", 115 | inject: ["./scripts/build/injectReact.mjs"], 116 | jsxFactory: "VencordCreateElement", 117 | jsxFragment: "VencordFragment", 118 | external: ["@vencord/types/*"], 119 | plugins: [vencordDep, includeDirPlugin("patches", "src/renderer/patches")], 120 | footer: { js: "//# sourceURL=VesktopRenderer" } 121 | }) 122 | ]); 123 | 124 | const watch = process.argv.includes("--watch"); 125 | 126 | if (watch) { 127 | await Promise.all(contexts.map(ctx => ctx.watch())); 128 | } else { 129 | await Promise.all( 130 | contexts.map(async ctx => { 131 | await ctx.rebuild(); 132 | await ctx.dispose(); 133 | }) 134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /src/renderer/components/settings/DeveloperOptions.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { BaseText, Button, Heading, Paragraph, TextButton } from "@vencord/types/components"; 8 | import { 9 | Margins, 10 | ModalCloseButton, 11 | ModalContent, 12 | ModalHeader, 13 | ModalRoot, 14 | ModalSize, 15 | openModal, 16 | useForceUpdater 17 | } from "@vencord/types/utils"; 18 | import { Toasts } from "@vencord/types/webpack/common"; 19 | import { Settings } from "shared/settings"; 20 | 21 | import { cl, SettingsComponent } from "./Settings"; 22 | 23 | export const DeveloperOptionsButton: SettingsComponent = ({ settings }) => { 24 | return ; 25 | }; 26 | 27 | function openDeveloperOptionsModal(settings: Settings) { 28 | openModal(props => ( 29 | 30 | 31 | 32 | Vesktop Developer Options 33 | 34 | 35 | 36 | 37 | 38 |
39 | Vencord Location 40 | 41 | 42 | 43 | Debugging 44 | 45 |
46 | 47 | 50 |
51 |
52 |
53 |
54 | )); 55 | } 56 | 57 | const VencordLocationPicker: SettingsComponent = ({ settings }) => { 58 | const forceUpdate = useForceUpdater(); 59 | const vencordDir = VesktopNative.fileManager.getVencordDir(); 60 | 61 | return ( 62 | <> 63 | 64 | Vencord files are loaded from{" "} 65 | {vencordDir ? ( 66 | { 69 | e.preventDefault(); 70 | VesktopNative.fileManager.showItemInFolder(vencordDir!); 71 | }} 72 | > 73 | {vencordDir} 74 | 75 | ) : ( 76 | "the default location" 77 | )} 78 | 79 |
80 | 107 | 116 |
117 | 118 | ); 119 | }; 120 | -------------------------------------------------------------------------------- /static/views/first-launch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vesktop Setup 6 | 7 | 8 | 9 | 109 | 110 | 111 | 112 |

Welcome to Vesktop

113 |

Let's customise your experience!

114 | 115 |
116 | 124 | 125 | 132 | 133 | 141 | 142 | 149 | 150 | 157 |
158 |
159 | 160 | 161 |
162 | 163 | 164 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | //@ts-check 8 | 9 | import { defineConfig } from "eslint/config"; 10 | import stylistic from "@stylistic/eslint-plugin"; 11 | import pathAlias from "eslint-plugin-path-alias"; 12 | import react from "eslint-plugin-react"; 13 | import simpleHeader from "eslint-plugin-simple-header"; 14 | import importSort from "eslint-plugin-simple-import-sort"; 15 | import unusedImports from "eslint-plugin-unused-imports"; 16 | import tseslint from "typescript-eslint"; 17 | import prettier from "eslint-plugin-prettier"; 18 | 19 | export default defineConfig( 20 | { ignores: ["dist"] }, 21 | { 22 | files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}"], 23 | settings: { 24 | react: { 25 | version: "19" 26 | } 27 | }, 28 | ...react.configs.flat.recommended, 29 | rules: { 30 | ...react.configs.flat.recommended.rules, 31 | "react/react-in-jsx-scope": "off", 32 | "react/prop-types": "off", 33 | "react/display-name": "off", 34 | "react/no-unescaped-entities": "off" 35 | } 36 | }, 37 | { 38 | files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}"], 39 | plugins: { 40 | simpleHeader, 41 | stylistic, 42 | importSort, 43 | unusedImports, 44 | // @ts-expect-error Missing types 45 | pathAlias, 46 | prettier, 47 | "@typescript-eslint": tseslint.plugin 48 | }, 49 | settings: { 50 | "import/resolver": { 51 | alias: { 52 | map: [] 53 | } 54 | } 55 | }, 56 | languageOptions: { 57 | parser: tseslint.parser, 58 | parserOptions: { 59 | project: true, 60 | tsconfigRootDir: import.meta.dirname 61 | } 62 | }, 63 | rules: { 64 | "simpleHeader/header": [ 65 | "error", 66 | { 67 | files: ["scripts/header.txt"], 68 | templates: { author: [".*", "Vendicated and Vesktop contributors"] } 69 | } 70 | ], 71 | 72 | // ESLint Rules 73 | yoda: "error", 74 | eqeqeq: ["error", "always", { null: "ignore" }], 75 | "prefer-destructuring": [ 76 | "error", 77 | { 78 | VariableDeclarator: { array: false, object: true }, 79 | AssignmentExpression: { array: false, object: false } 80 | } 81 | ], 82 | "operator-assignment": ["error", "always"], 83 | "no-useless-computed-key": "error", 84 | "no-unneeded-ternary": ["error", { defaultAssignment: false }], 85 | "no-invalid-regexp": "error", 86 | "no-constant-condition": ["error", { checkLoops: false }], 87 | "no-duplicate-imports": "error", 88 | "@typescript-eslint/dot-notation": [ 89 | "error", 90 | { 91 | allowPrivateClassPropertyAccess: true, 92 | allowProtectedClassPropertyAccess: true 93 | } 94 | ], 95 | "no-useless-escape": [ 96 | "error", 97 | { 98 | allowRegexCharacters: ["i"] 99 | } 100 | ], 101 | "no-fallthrough": "error", 102 | "for-direction": "error", 103 | "no-async-promise-executor": "error", 104 | "no-cond-assign": "error", 105 | "no-dupe-else-if": "error", 106 | "no-duplicate-case": "error", 107 | "no-irregular-whitespace": "error", 108 | "no-loss-of-precision": "error", 109 | "no-misleading-character-class": "error", 110 | "no-prototype-builtins": "error", 111 | "no-regex-spaces": "error", 112 | "no-shadow-restricted-names": "error", 113 | "no-unexpected-multiline": "error", 114 | "no-unsafe-optional-chaining": "error", 115 | "no-useless-backreference": "error", 116 | "use-isnan": "error", 117 | "prefer-const": ["error", { destructuring: "all" }], 118 | "prefer-spread": "error", 119 | 120 | // Styling Rules 121 | "stylistic/spaced-comment": ["error", "always", { markers: ["!"] }], 122 | "stylistic/no-extra-semi": "error", 123 | 124 | // Plugin Rules 125 | "importSort/imports": "error", 126 | "importSort/exports": "error", 127 | "unusedImports/no-unused-imports": "error", 128 | "pathAlias/no-relative": "error", 129 | "prettier/prettier": "error" 130 | } 131 | } 132 | ); 133 | -------------------------------------------------------------------------------- /src/preload/VesktopNative.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { Node } from "@vencord/venmic"; 8 | import { ipcRenderer } from "electron"; 9 | import { IpcMessage, IpcResponse } from "main/ipcCommands"; 10 | import type { Settings } from "shared/settings"; 11 | 12 | import { IpcEvents } from "../shared/IpcEvents"; 13 | import { invoke, sendSync } from "./typedIpc"; 14 | 15 | type SpellCheckerResultCallback = (word: string, suggestions: string[]) => void; 16 | 17 | const spellCheckCallbacks = new Set(); 18 | 19 | ipcRenderer.on(IpcEvents.SPELLCHECK_RESULT, (_, w: string, s: string[]) => { 20 | spellCheckCallbacks.forEach(cb => cb(w, s)); 21 | }); 22 | 23 | let onDevtoolsOpen = () => {}; 24 | let onDevtoolsClose = () => {}; 25 | 26 | ipcRenderer.on(IpcEvents.DEVTOOLS_OPENED, () => onDevtoolsOpen()); 27 | ipcRenderer.on(IpcEvents.DEVTOOLS_CLOSED, () => onDevtoolsClose()); 28 | 29 | export const VesktopNative = { 30 | app: { 31 | relaunch: () => invoke(IpcEvents.RELAUNCH), 32 | getVersion: () => sendSync(IpcEvents.GET_VERSION), 33 | setBadgeCount: (count: number) => invoke(IpcEvents.SET_BADGE_COUNT, count), 34 | supportsWindowsTransparency: () => sendSync(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY), 35 | getEnableHardwareAcceleration: () => sendSync(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION), 36 | isOutdated: () => invoke(IpcEvents.UPDATER_IS_OUTDATED), 37 | openUpdater: () => invoke(IpcEvents.UPDATER_OPEN) 38 | }, 39 | autostart: { 40 | isEnabled: () => sendSync(IpcEvents.AUTOSTART_ENABLED), 41 | enable: () => invoke(IpcEvents.ENABLE_AUTOSTART), 42 | disable: () => invoke(IpcEvents.DISABLE_AUTOSTART) 43 | }, 44 | fileManager: { 45 | showItemInFolder: (path: string) => invoke(IpcEvents.SHOW_ITEM_IN_FOLDER, path), 46 | getVencordDir: () => sendSync(IpcEvents.GET_VENCORD_DIR), 47 | selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value), 48 | chooseUserAsset: (asset: string, value?: null) => 49 | invoke<"cancelled" | "invalid" | "ok" | "failed">(IpcEvents.CHOOSE_USER_ASSET, asset, value) 50 | }, 51 | settings: { 52 | get: () => sendSync(IpcEvents.GET_SETTINGS), 53 | set: (settings: Settings, path?: string) => invoke(IpcEvents.SET_SETTINGS, settings, path) 54 | }, 55 | spellcheck: { 56 | getAvailableLanguages: () => sendSync(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES), 57 | onSpellcheckResult(cb: SpellCheckerResultCallback) { 58 | spellCheckCallbacks.add(cb); 59 | }, 60 | offSpellcheckResult(cb: SpellCheckerResultCallback) { 61 | spellCheckCallbacks.delete(cb); 62 | }, 63 | replaceMisspelling: (word: string) => invoke(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, word), 64 | addToDictionary: (word: string) => invoke(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, word) 65 | }, 66 | win: { 67 | focus: () => invoke(IpcEvents.FOCUS), 68 | close: (key?: string) => invoke(IpcEvents.CLOSE, key), 69 | minimize: (key?: string) => invoke(IpcEvents.MINIMIZE, key), 70 | maximize: (key?: string) => invoke(IpcEvents.MAXIMIZE, key), 71 | flashFrame: (flag: boolean) => invoke(IpcEvents.FLASH_FRAME, flag), 72 | setDevtoolsCallbacks: (onOpen: () => void, onClose: () => void) => { 73 | onDevtoolsOpen = onOpen; 74 | onDevtoolsClose = onClose; 75 | } 76 | }, 77 | capturer: { 78 | getLargeThumbnail: (id: string) => invoke(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id) 79 | }, 80 | /** only available on Linux. */ 81 | virtmic: { 82 | list: () => 83 | invoke< 84 | { ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean } 85 | >(IpcEvents.VIRT_MIC_LIST), 86 | start: (include: Node[]) => invoke(IpcEvents.VIRT_MIC_START, include), 87 | startSystem: (exclude: Node[]) => invoke(IpcEvents.VIRT_MIC_START_SYSTEM, exclude), 88 | stop: () => invoke(IpcEvents.VIRT_MIC_STOP) 89 | }, 90 | clipboard: { 91 | copyImage: (imageBuffer: Uint8Array, imageSrc: string) => 92 | invoke(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc) 93 | }, 94 | debug: { 95 | launchGpu: () => invoke(IpcEvents.DEBUG_LAUNCH_GPU), 96 | launchWebrtcInternals: () => invoke(IpcEvents.DEBUG_LAUNCH_WEBRTC_INTERNALS) 97 | }, 98 | commands: { 99 | onCommand(cb: (message: IpcMessage) => void) { 100 | ipcRenderer.on(IpcEvents.IPC_COMMAND, (_, message) => cb(message)); 101 | }, 102 | respond: (response: IpcResponse) => ipcRenderer.send(IpcEvents.IPC_COMMAND, response) 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /src/main/cli.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2025 Vendicated and Vesktop contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { app } from "electron"; 8 | import { basename } from "path"; 9 | import { stripIndent } from "shared/utils/text"; 10 | import { parseArgs, ParseArgsOptionDescriptor } from "util"; 11 | 12 | type Option = ParseArgsOptionDescriptor & { 13 | description: string; 14 | hidden?: boolean; 15 | options?: string[]; 16 | argumentName?: string; 17 | }; 18 | 19 | const options = { 20 | "start-minimized": { 21 | default: false, 22 | type: "boolean", 23 | short: "m", 24 | description: "Start the application minimized to the system tray" 25 | }, 26 | version: { 27 | type: "boolean", 28 | short: "v", 29 | description: "Print the application version and exit" 30 | }, 31 | help: { 32 | type: "boolean", 33 | short: "h", 34 | description: "Print help information and exit" 35 | }, 36 | "user-agent": { 37 | type: "string", 38 | argumentName: "ua", 39 | description: "Set a custom User-Agent. May trigger anti-spam or break voice chat" 40 | }, 41 | "user-agent-os": { 42 | type: "string", 43 | description: "Set User-Agent to a specific operating system. May trigger anti-spam or break voice chat", 44 | options: ["windows", "linux", "darwin"] 45 | } 46 | } satisfies Record; 47 | 48 | // only for help display 49 | const extraOptions = { 50 | "enable-features": { 51 | type: "string", 52 | description: "Enable specific Chromium features", 53 | argumentName: "feature1,feature2,…" 54 | }, 55 | "disable-features": { 56 | type: "string", 57 | description: "Disable specific Chromium features", 58 | argumentName: "feature1,feature2,…" 59 | }, 60 | "ozone-platform": { 61 | hidden: process.platform !== "linux", 62 | type: "string", 63 | description: "Whether to run Vesktop in Wayland or X11 (XWayland)", 64 | options: ["x11", "wayland"] 65 | } 66 | } satisfies Record; 67 | 68 | const args = basename(process.argv[0]).toLowerCase().startsWith("electron") 69 | ? process.argv.slice(2) 70 | : process.argv.slice(1); 71 | 72 | export const CommandLine = parseArgs({ 73 | args, 74 | options, 75 | strict: false as true, // we manually check later, so cast to true to get better types 76 | allowPositionals: true 77 | }); 78 | 79 | export function checkCommandLineForHelpOrVersion() { 80 | const { help, version } = CommandLine.values; 81 | 82 | if (version) { 83 | console.log(`Vesktop v${app.getVersion()}`); 84 | app.exit(0); 85 | } 86 | 87 | if (help) { 88 | const base = stripIndent` 89 | Vesktop v${app.getVersion()} 90 | 91 | Usage: ${basename(process.execPath)} [options] [url] 92 | 93 | Electron Options: 94 | See 95 | 96 | Chromium Options: 97 | See - only some of them work 98 | 99 | Vesktop Options: 100 | `; 101 | 102 | const optionLines = Object.entries(options) 103 | .sort(([a], [b]) => a.localeCompare(b)) 104 | .concat(Object.entries(extraOptions)) 105 | .filter(([, opt]) => !("hidden" in opt && opt.hidden)) 106 | .map(([name, opt]) => { 107 | const flags = [ 108 | "short" in opt && `-${opt.short}`, 109 | `--${name}`, 110 | opt.type !== "boolean" && 111 | ("options" in opt ? `<${opt.options.join(" | ")}>` : `<${opt.argumentName ?? opt.type}>`) 112 | ] 113 | .filter(Boolean) 114 | .join(" "); 115 | 116 | return [flags, opt.description]; 117 | }); 118 | 119 | const padding = optionLines.reduce((max, [flags]) => Math.max(max, flags.length), 0) + 4; 120 | 121 | const optionsHelp = optionLines 122 | .map(([flags, description]) => ` ${flags.padEnd(padding, " ")}${description}`) 123 | .join("\n"); 124 | 125 | console.log(base + "\n" + optionsHelp); 126 | app.exit(0); 127 | } 128 | 129 | for (const [name, def] of Object.entries(options)) { 130 | const value = CommandLine.values[name]; 131 | if (value == null) continue; 132 | 133 | if (typeof value !== def.type) { 134 | console.error(`Invalid options. Expected ${def.type === "boolean" ? "no" : "an"} argument for --${name}`); 135 | app.exit(1); 136 | } 137 | 138 | if ("options" in def && !def.options?.includes(value as string)) { 139 | console.error(`Invalid value for --${name}: ${value}\nExpected one of: ${def.options.join(", ")}`); 140 | app.exit(1); 141 | } 142 | } 143 | } 144 | 145 | checkCommandLineForHelpOrVersion(); 146 | -------------------------------------------------------------------------------- /src/renderer/patches/spellCheck.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Vesktop, a desktop app aiming to give you a snappier Discord Experience 3 | * Copyright (c) 2023 Vendicated and Vencord contributors 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | import { addContextMenuPatch } from "@vencord/types/api/ContextMenu"; 8 | import { findStoreLazy } from "@vencord/types/webpack"; 9 | import { FluxDispatcher, Menu, useMemo, useStateFromStores } from "@vencord/types/webpack/common"; 10 | import { useSettings } from "renderer/settings"; 11 | 12 | import { addPatch } from "./shared"; 13 | 14 | let word: string; 15 | let corrections: string[]; 16 | 17 | const SpellCheckStore = findStoreLazy("SpellcheckStore"); 18 | 19 | // Make spellcheck suggestions work 20 | addPatch({ 21 | patches: [ 22 | { 23 | find: ".enableSpellCheck)", 24 | replacement: { 25 | // if (isDesktop) { DiscordNative.onSpellcheck(openMenu(props)) } else { e.preventDefault(); openMenu(props) } 26 | match: /else (\i)\.preventDefault\(\),(\i\(\i\))(?<=:(\i)\.enableSpellCheck\).+?)/, 27 | // ... else { $self.onSlateContext(() => openMenu(props)) } 28 | replace: "else {$self.onSlateContext($1, $3?.enableSpellCheck, () => $2)}" 29 | } 30 | } 31 | ], 32 | 33 | onSlateContext(e: MouseEvent, hasSpellcheck: boolean | undefined, openMenu: () => void) { 34 | if (!hasSpellcheck) { 35 | e.preventDefault(); 36 | openMenu(); 37 | return; 38 | } 39 | 40 | const cb = (w: string, c: string[]) => { 41 | VesktopNative.spellcheck.offSpellcheckResult(cb); 42 | word = w; 43 | corrections = c; 44 | openMenu(); 45 | }; 46 | VesktopNative.spellcheck.onSpellcheckResult(cb); 47 | } 48 | }); 49 | 50 | addContextMenuPatch("textarea-context", children => { 51 | const spellCheckEnabled = useStateFromStores([SpellCheckStore], () => SpellCheckStore.isEnabled()); 52 | const hasCorrections = Boolean(word && corrections?.length); 53 | 54 | const availableLanguages = useMemo(VesktopNative.spellcheck.getAvailableLanguages, []); 55 | 56 | const settings = useSettings(); 57 | const spellCheckLanguages = (settings.spellCheckLanguages ??= [...new Set(navigator.languages)]); 58 | 59 | const pasteSectionIndex = children.findIndex(c => c?.props?.children?.some?.(c => c?.props?.id === "paste")); 60 | 61 | children.splice( 62 | pasteSectionIndex === -1 ? children.length : pasteSectionIndex, 63 | 0, 64 | 65 | {hasCorrections && ( 66 | <> 67 | {corrections.map(c => ( 68 | VesktopNative.spellcheck.replaceMisspelling(c)} 73 | /> 74 | ))} 75 | 76 | VesktopNative.spellcheck.addToDictionary(word)} 80 | /> 81 | 82 | )} 83 | 84 | 85 | { 90 | FluxDispatcher.dispatch({ type: "SPELLCHECK_TOGGLE" }); 91 | }} 92 | /> 93 | 94 | 95 | {availableLanguages.map(lang => { 96 | const isEnabled = spellCheckLanguages.includes(lang); 97 | return ( 98 | = 5} 104 | action={() => { 105 | const newSpellCheckLanguages = spellCheckLanguages.filter(l => l !== lang); 106 | if (newSpellCheckLanguages.length === spellCheckLanguages.length) { 107 | newSpellCheckLanguages.push(lang); 108 | } 109 | 110 | settings.spellCheckLanguages = newSpellCheckLanguages; 111 | }} 112 | /> 113 | ); 114 | })} 115 | 116 | 117 | 118 | ); 119 | }); 120 | --------------------------------------------------------------------------------