3 Commits

Author SHA1 Message Date
Lewis Crichton
bb09596485 chore: prep for flatpak 2023-12-09 23:09:54 +00:00
Lewis Crichton
894ec4b902 chore: lint 2023-12-09 23:01:35 +00:00
Lewis Crichton
dd44602730 fix: make minimize only run on autostart 2023-12-09 23:00:47 +00:00
171 changed files with 5376 additions and 11604 deletions

View File

@@ -5,4 +5,4 @@
# https://github.com/settings/personal-access-tokens/new # https://github.com/settings/personal-access-tokens/new
GITHUB_TOKEN= GITHUB_TOKEN=
ELECTRON_LAUNCH_FLAGS="--enable-source-maps --ozone-platform-hint=auto" ELECTRON_LAUNCH_FLAGS="--ozone-platform-hint=auto --enable-webrtc-pipewire-capturer --enable-features=WaylandWindowDecorations"

63
.eslintrc.json Normal file
View File

@@ -0,0 +1,63 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"ignorePatterns": ["dist", "node_modules"],
"plugins": [
"@typescript-eslint",
"license-header",
"simple-import-sort",
"unused-imports",
"path-alias",
"prettier"
],
"settings": {
"import/resolver": {
"alias": {
"map": []
}
}
},
"rules": {
"license-header/header": ["error", "scripts/header.txt"],
"eqeqeq": ["error", "always", { "null": "ignore" }],
"spaced-comment": ["error", "always", { "markers": ["!"] }],
"yoda": "error",
"prefer-destructuring": ["error", { "object": true, "array": false }],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { "defaultAssignment": false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"no-duplicate-imports": "error",
"no-extra-semi": "error",
"dot-notation": "error",
"no-useless-escape": "error",
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"path-alias/no-relative": "error",
"prettier/prettier": "error"
}
}

View File

@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Vencord Support Server
url: https://discord.gg/D9uwnFnqmd
about: "Need Help? Join our support server and ask in the #vesktop-support channel!"

View File

@@ -1,20 +0,0 @@
name: Vesktop Developer Issue
description: Reserved for Vesktop Developers. Join our support server for support.
body:
- type: markdown
attributes:
value: |
# This form is reserved for Vesktop Developers. Do not open an issue.
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.
Your issue will be closed immediately with no comment and you will be blocked if you ignore this.
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.
- type: textarea
id: content
attributes:
label: Content
validations:
required: true

View File

@@ -6,33 +6,33 @@ on:
- published - published
workflow_dispatch: workflow_dispatch:
permissions:
contents: write
jobs: jobs:
update: update:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 20 - name: Use Node.js 18.18.2
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 18.18.2
- name: Install dependencies - name: Install dependencies
run: pnpm i run: pnpm i
- name: Update metainfo - name: Update metainfo
run: pnpm generateMeta run: pnpm updateMeta
- name: Commit and merge in changes - name: Commit and merge in changes
run: | run: |
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b ci/meta-update
gh release upload "${{ github.event.release.tag_name }}" dist/dev.vencord.Vesktop.metainfo.xml git add meta/dev.vencord.Vesktop.metainfo.xml
env: git commit -m "Insert release changes for ${{ github.event.release.tag_name }}"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} git push origin ci/meta-update
gh pr create -B main -H ci/meta-update -t "Metainfo for ${{ github.event.release.tag_name }}" -b "This PR updates the metainfo for release ${{ github.event.release.tag_name }}. @lewisakura @Vendicated"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -4,7 +4,6 @@ on:
push: push:
tags: tags:
- v* - v*
workflow_dispatch:
jobs: jobs:
release: release:
@@ -22,13 +21,13 @@ jobs:
platform: windows platform: windows
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 20 - name: Use Node.js 18.18.2
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 18.18.2
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
@@ -38,22 +37,7 @@ jobs:
run: pnpm build run: pnpm build
- name: Run Electron Builder - name: Run Electron Builder
if: ${{ matrix.platform != 'mac' }}
run: | run: |
pnpm electron-builder --${{ matrix.platform }} --publish always pnpm electron-builder --${{ matrix.platform }} --publish always
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run Electron Builder
if: ${{ matrix.platform == 'mac' }}
run: |
echo "$API_KEY" > apple.p8
pnpm electron-builder --${{ matrix.platform }} --publish always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.APPLE_SIGNING_CERT }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_SIGNING_CERT_PASSWORD }}
API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY: apple.p8
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}

View File

@@ -11,13 +11,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v4 # Install pnpm using packageManager key in package.json - uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json
- name: Use Node.js 20 - name: Use Node.js 18.18.2
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 18.18.2
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies

View File

@@ -1,29 +0,0 @@
name: Update vencord.dev Vesktop version
on:
release:
types:
- published
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Update scripts/_latestVesktopVersion.txt file in vencord.dev repo
run: |
git config --global user.name "$USERNAME"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git clone https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git repo
cd repo
version="${{ github.event.release.tag_name }}"
echo "${version#v}" > scripts/_latestVesktopVersion.txt
git add scripts/_latestVesktopVersion.txt
git commit -m "Update Vesktop version to ${{ github.event.release.tag_name }}"
git push https://$USERNAME:$API_TOKEN@github.com/$GH_REPO.git
env:
API_TOKEN: ${{ secrets.VENCORD_DEV_GITHUB_TOKEN }}
GH_REPO: Vencord/vencord.dev
USERNAME: GitHub-Actions

View File

@@ -13,10 +13,10 @@ on:
jobs: jobs:
winget: winget:
name: Publish winget package name: Publish winget package
runs-on: windows-latest runs-on: ubuntu-latest
steps: steps:
- name: Submit package to Winget Community Repo - name: Submit package to Winget Community Repo
uses: vedantmgoyal2009/winget-releaser@0db4f0a478166abd0fa438c631849f0b8dcfb99f uses: vedantmgoyal2009/winget-releaser@e68d386d5d6a1cef8cb0fb5e62b77ebcb83e7d58 # v2
with: with:
identifier: Vencord.Vesktop identifier: Vencord.Vesktop
token: ${{ secrets.WINGET_PAT }} token: ${{ secrets.WINGET_PAT }}

1
.npmrc
View File

@@ -1,2 +1 @@
node-linker=hoisted node-linker=hoisted
package-manager-strict=false

View File

@@ -1,32 +1,51 @@
# Vesktop # Vesktop
Vesktop is a custom Discord desktop app Vesktop is a cross platform desktop app aiming to give you a snappier Discord experience with [Vencord](https://github.com/Vendicated/Vencord) pre-installed
**Main features**:
- Vencord preinstalled
- Much more lightweight and faster than the official Discord app
- Linux Screenshare with sound & wayland
- Much better privacy, since Discord has no access to your system
**Not yet supported**: **Not yet supported**:
- Global Keybinds - Global Keybinds
- see the [Roadmap](https://github.com/Vencord/Vesktop/issues/324)
Bug reports, feature requests & contributions are highly appreciated!!
![](https://github.com/Vencord/Vesktop/assets/45497981/8608a899-96a9-4027-9725-2cb02ba189fd) ![](https://github.com/Vencord/Vesktop/assets/45497981/8608a899-96a9-4027-9725-2cb02ba189fd)
![](https://github.com/Vencord/Vesktop/assets/45497981/8701e5de-52c4-4346-a990-719cb971642e) ![grafik](https://github.com/Vencord/Vesktop/assets/45497981/8701e5de-52c4-4346-a990-719cb971642e)
## Installing ## Installing
Visit https://vesktop.dev/install ### Windows
## Building from Source Download and run Vesktop-Setup-VERSION.exe from [releases](https://github.com/Vencord/Vesktop/releases/latest)
You need to have the following dependencies installed: ### Mac
- [Git](https://git-scm.com/downloads)
- [Node.js](https://nodejs.org/en/download)
- pnpm: `npm install --global pnpm`
Packaging will create builds in the dist/ folder Download and run Vesktop-VERSION.dmg from [releases](https://github.com/Vencord/Vesktop/releases/latest)
### Linux
[![](https://dl.flathub.org/assets/badges/flathub-badge-en.svg)](https://flathub.org/apps/dev.vencord.Vesktop)
#### Arch based
Install [vencord-desktop-git](https://aur.archlinux.org/packages/vencord-desktop-git) from the AUR using your favourite AUR helper, for example [yay](https://github.com/Jguer/yay)
#### Ubuntu/Debian based
Download Vesktop-VERSION.deb from [releases](https://github.com/Vencord/Vesktop/releases/latest)
#### Fedora/RHEL based
Download Vesktop-VERSION.rpm from [releases](https://github.com/Vencord/Vesktop/releases/latest)
#### Other
Either download Vesktop-VERSION.AppImage and just run it directly or grab Vesktop-VERSION.tar.gz, extract it somewhere and run `vencorddesktop`.
If other packages are created, feel free to open an issue and we'll link them here.
## Building
Packaging will create builds in the dist/ folder. You can then install them like mentioned above or distribute them
```sh ```sh
git clone https://github.com/Vencord/Vesktop git clone https://github.com/Vencord/Vesktop
@@ -38,23 +57,18 @@ pnpm i
# Either run it without packaging # Either run it without packaging
pnpm start pnpm start
# Or package (will build packages for your OS) # Or package
pnpm package pnpm package
# Or only build the pacman target
# Or only build the Linux Pacman package
pnpm package --linux pacman pnpm package --linux pacman
# Or package to a directory only # Or package to a directory only
pnpm package:dir pnpm package:dir
``` ```
## Building LibVesktop from Source ## Motivation
This is a small C++ helper library Vesktop uses on Linux to emit D-Bus events. By default, prebuilt binaries for x64 and arm64 are used. The official Discord Desktop app is very resource heavy compared to Discord in your Browser. There are multiple alternative Electron apps (ArmCord, WebCord, probably more) that prove how much of a performance gain you can gain by using a custom app. ArmCord already supports Vencord but makes it pretty limited for us. Making our own standalone app gives us much more control.
If you want to build it from source: This is just a random idea I (V) got, and might not actually ever be finished heh
1. Install build dependencies:
- Debian/Ubuntu: `apt install build-essential python3 curl pkg-config libglib2.0-dev` Gluon also seems very attractive for this because of how lightweight it can be and because unlike electron, streaming just works out of the box like in any chromium browser. However, at the time of writing this, it still lacks some features necessary to make it work (synchronous ipc or a way to get node process variables into the onLoad function for instance, plus onLoad seems to load a little too late sometimes)
- Fedora: `dnf install @c-development @development-tools python3 curl pkgconf-pkg-config glib2-devel`
2. Run `pnpm buildLibVesktop`
3. From now on, building Vesktop will use your own build

Binary file not shown.

Binary file not shown.

View File

@@ -1,21 +0,0 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" style="isolation:isolate" viewBox="0 0 1024 1024"><path fill="#eb7396" d="M336.23 886.35c56.07 31.62 150.18 51.4 247.12 37.3C816.1 889.78 977.57 673.33 943.7 440.57 909.85 207.83 693.41 46.36 460.65 80.22 227.9 114.08 66.43 330.53 100.3 563.28c6.86 47.2 21.23 91.46 29.65 108.01 6.67 13.1 9.03 35.28 5.28 49.5l-26.8 101.43c-16.75 63.41 21.03 100.93 84.32 83.73l94.59-25.7c14.18-3.86 36.1-1.12 48.9 6.1z" style="display:inline;isolation:isolate"/><g style="display:inline;isolation:isolate"><g style="isolation:isolate"><path d="M494.586 109.162c-3.861.14-7.743.508-11.621 1.004-72.122 6.505-117.149 63.067-128.176 116.693l-17.812 86.508-35.555-76.785v-.002c-23.455-50.637-77.415-90.65-141.998-81.877-32.285 4.386-60.608 22.22-76.123 48.385-13.722 23.14-16.564 52.439-7.66 78.943v1.608l157.394 336.037c24.863 53.102 81.404 87.116 143.43 78.681 61.475-8.36 109.47-52.362 123.914-110.074l91.158-364.25c7.385-29.508 1.643-63.11-20.129-86.328-19.05-20.315-47.417-29.61-76.822-28.543" style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#1d2021;stop-color:#000;stop-opacity:1"/><path fill="#fff" d="m118.49 274.1 153.35 327.4c16 34.18 58.517 59.883 98.852 54.398s78.958-41.418 88.118-78.018l91.16-364.25c9.16-36.6-14.593-67.398-62.253-60.918-51.41 4.189-83.357 45.818-90.957 82.778l-37.84 183.78c-3.42 16.62-11.98 17.61-19.1 2.22l-77.28-166.9c-15.86-34.24-55.539-63.108-97.349-57.428S102.48 239.92 118.49 274.09Z" style="display:inline;fill:#ededed;fill-opacity:1"/><path d="M701.543 396.742a266.4 266.4 0 0 0-100.73 17.746l-.004.002h-.002a271.8 271.8 0 0 0-81.118 48.942 275 275 0 0 0-59.822 74.15v.002a277 277 0 0 0-31.572 93.527v.006l-.002.006c-1.984 13.48-3.023 27.136-3.024 40.893v.014a275.13 275.13 0 0 0 64.01 176.617l.004.004.002.004a269.5 269.5 0 0 0 72.846 61.008l.02.011.02.012a266.5 266.5 0 0 0 93.054 32.19l.017.002a266.3 266.3 0 0 0 114.906-8.061l.02-.006.02-.006a270.3 270.3 0 0 0 67.706-30.816l.004-.002c34.575-21.867 52.68-62.732 45.645-103.032-7.032-40.29-37.897-72.6-77.82-81.47a101.46 101.46 0 0 0-76.338 13.314l-.024.016-.023.015a67 67 0 0 1-16.83 7.67 63.3 63.3 0 0 1-17.957 2.6h-.081a63.86 63.86 0 0 1-31.726-8.37 67 67 0 0 1-17.951-15.042l-.006-.008a72.44 72.44 0 0 1-16.838-46.524v-.016a76.6 76.6 0 0 1 9.367-36.642 72.5 72.5 0 0 1 15.688-19.395l.012-.012.011-.011a69.3 69.3 0 0 1 20.617-12.455 63.97 63.97 0 0 1 59.061 6.896 68.5 68.5 0 0 1 19.91 21.201h.002v.002c28.529 47.605 91.489 63.35 139.068 34.78l.01-.006.01-.006c47.508-28.562 63.227-91.41 34.76-138.975l-.013-.025-.016-.026a271.8 271.8 0 0 0-79.111-84.105l-.026-.02-.027-.017a266.5 266.5 0 0 0-111.506-43.725 266 266 0 0 0-34.223-2.857" style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#ededed;stop-color:#000;stop-opacity:1"/><path fill="#1d2021" d="M751.65 766.94a59.8 59.8 0 0 1 45.03-7.85c23.607 5.237 41.724 24.197 45.882 48.018 4.157 23.821-6.465 47.797-26.902 60.722a228.7 228.7 0 0 1-57.33 26.1 224.7 224.7 0 0 1-96.97 6.8 224.9 224.9 0 0 1-78.56-27.17 227.9 227.9 0 0 1-61.6-51.59 233.5 233.5 0 0 1-54.33-149.94c0-11.67.88-23.3 2.58-34.85a235.4 235.4 0 0 1 26.83-79.48 233.4 233.4 0 0 1 50.78-62.94 230.2 230.2 0 0 1 68.7-41.45 224.8 224.8 0 0 1 113.91-12.56c33.75 5 65.94 17.62 94.1 36.9a230.15 230.15 0 0 1 67 71.23c16.936 28.299 7.765 64.967-20.5 81.96-28.294 16.99-65.005 7.81-81.97-20.5a110.1 110.1 0 0 0-32.08-34.14 105.7 105.7 0 0 0-97.52-11.4 110.9 110.9 0 0 0-33.05 19.96 114 114 0 0 0-24.79 30.69 118.2 118.2 0 0 0-14.51 56.66 114.07 114.07 0 0 0 26.51 73.24 108.6 108.6 0 0 0 29.24 24.5 105.5 105.5 0 0 0 52.45 13.85c10.08 0 20.12-1.45 29.8-4.32a108.6 108.6 0 0 0 27.3-12.44" style="display:inline"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,8 +1,8 @@
!macro preInit !macro preInit
SetRegView 64 SetRegView 64
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop" WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop" WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop"
SetRegView 32 SetRegView 32
WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop" WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop"
WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\vesktop" WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$LocalAppData\VencordDesktop"
!macroend !macroend

View File

@@ -1,132 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
//@ts-check
import { defineConfig } from "eslint/config";
import stylistic from "@stylistic/eslint-plugin";
import pathAlias from "eslint-plugin-path-alias";
import react from "eslint-plugin-react";
import simpleHeader from "eslint-plugin-simple-header";
import importSort from "eslint-plugin-simple-import-sort";
import unusedImports from "eslint-plugin-unused-imports";
import tseslint from "typescript-eslint";
import prettier from "eslint-plugin-prettier";
export default defineConfig(
{ ignores: ["dist"] },
{
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}"],
settings: {
react: {
version: "19"
}
},
...react.configs.flat.recommended,
rules: {
...react.configs.flat.recommended.rules,
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/display-name": "off",
"react/no-unescaped-entities": "off"
}
},
{
files: ["src/**/*.{tsx,ts,mts,mjs,js,jsx}"],
plugins: {
simpleHeader,
stylistic,
importSort,
unusedImports,
// @ts-expect-error Missing types
pathAlias,
prettier,
"@typescript-eslint": tseslint.plugin
},
settings: {
"import/resolver": {
alias: {
map: []
}
}
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname
}
},
rules: {
"simpleHeader/header": [
"error",
{
files: ["scripts/header.txt"],
templates: { author: [".*", "Vendicated and Vesktop contributors"] }
}
],
// ESLint Rules
yoda: "error",
eqeqeq: ["error", "always", { null: "ignore" }],
"prefer-destructuring": [
"error",
{
VariableDeclarator: { array: false, object: true },
AssignmentExpression: { array: false, object: false }
}
],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", { defaultAssignment: false }],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", { checkLoops: false }],
"no-duplicate-imports": "error",
"@typescript-eslint/dot-notation": [
"error",
{
allowPrivateClassPropertyAccess: true,
allowProtectedClassPropertyAccess: true
}
],
"no-useless-escape": [
"error",
{
allowRegexCharacters: ["i"]
}
],
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": ["error", { destructuring: "all" }],
"prefer-spread": "error",
// Styling Rules
"stylistic/spaced-comment": ["error", "always", { markers: ["!"] }],
"stylistic/no-extra-semi": "error",
// Plugin Rules
"importSort/imports": "error",
"importSort/exports": "error",
"unusedImports/no-unused-imports": "error",
"pathAlias/no-relative": "error",
"prettier/prettier": "error"
}
}
);

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8"?>
<component type="desktop-application">
<!--Created with jdAppStreamEdit 7.1-->
<id>dev.vencord.Vesktop</id>
<name>Vesktop</name>
<summary>Snappier Discord app with Vencord</summary>
<developer_name>Vencord Contributors</developer_name>
<launchable type="desktop-id">dev.vencord.Vesktop.desktop</launchable>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0</project_license>
<project_group>Vencord</project_group>
<description>
<p>Vesktop is a cross platform desktop app aiming to give you a snappier Discord experience with Vencord pre-installed.</p>
<p>Vesktop comes bundled with Venmic, a purpose-built library to provide functioning audio screenshare.</p>
</description>
<screenshots>
<screenshot type="default">
<caption>Vencord settings page and about window open</caption>
<image type="source">https://vencord.dev/assets/screenshots/vesktop-1-appstream.png</image>
</screenshot>
<screenshot>
<caption>A dialog showing screenshare options</caption>
<image type="source">https://vencord.dev/assets/screenshots/vesktop-2-appstream.png</image>
</screenshot>
<screenshot>
<caption>A screenshot of a Discord server</caption>
<image type="source">https://vencord.dev/assets/screenshots/vesktop-3-appstream.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.4.4" date="2023-12-02" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.4</url>
<description>
<p>What's Changed</p>
<ul>
<li>improve venmic system compatibility by @Curve</li>
<li>Update steamdeck controller layout by @AAGaming00</li>
<li>feat: Add option to disable smooth scrolling by @ZirixCZ</li>
<li>unblur shiggy in splash screen by @viacoro</li>
<li>update electron &amp; arrpc @D3SOX</li>
</ul>
</description>
</release>
<release version="0.4.3" date="2023-11-01" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.3</url>
</release>
<release version="0.4.2" date="2023-10-26" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.2</url>
</release>
<release version="0.4.1" date="2023-10-24" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.1</url>
</release>
<release version="0.4.0" date="2023-10-21" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.4.0</url>
</release>
<release version="0.3.3" date="2023-09-30" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.3</url>
</release>
<release version="0.3.2" date="2023-09-25" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.2</url>
</release>
<release version="0.3.1" date="2023-09-25" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.1</url>
</release>
<release version="0.3.0" date="2023-08-16" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.3.0</url>
</release>
<release version="0.2.9" date="2023-08-12" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.9</url>
</release>
<release version="0.2.8" date="2023-08-02" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.8</url>
</release>
<release version="0.2.7" date="2023-07-26" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.7</url>
</release>
<release version="0.2.6" date="2023-07-04" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.6</url>
</release>
<release version="0.2.5" date="2023-06-26" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.5</url>
</release>
<release version="0.2.4" date="2023-06-25" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.4</url>
</release>
<release version="0.2.3" date="2023-06-23" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.3</url>
</release>
<release version="0.2.2" date="2023-06-21" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.2</url>
</release>
<release version="0.2.1" date="2023-06-21" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.1</url>
</release>
<release version="0.2.0" date="2023-05-03" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.2.0</url>
</release>
<release version="0.1.9" date="2023-04-27" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.9</url>
</release>
<release version="0.1.8" date="2023-04-15" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.8</url>
</release>
<release version="0.1.7" date="2023-04-15" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.7</url>
</release>
<release version="0.1.6" date="2023-04-11" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.6</url>
</release>
<release version="0.1.5" date="2023-04-10" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.5</url>
</release>
<release version="0.1.4" date="2023-04-09" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.4</url>
</release>
<release version="0.1.3" date="2023-04-06" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.3</url>
</release>
<release version="0.1.2" date="2023-04-05" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.2</url>
</release>
<release version="0.1.1" date="2023-04-04" type="stable">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.1</url>
</release>
<release version="0.1.0" date="2023-04-04" type="development">
<url>https://github.com/Vencord/Vesktop/releases/tag/v0.1.0</url>
</release>
</releases>
<url type="homepage">https://vencord.dev/</url>
<url type="bugtracker">https://github.com/Vencord/Vesktop/issues</url>
<url type="faq">https://vencord.dev/faq/</url>
<url type="help">https://github.com/Vencord/Vesktop/issues</url>
<url type="donation">https://github.com/sponsors/Vendicated</url>
<url type="vcs-browser">https://github.com/Vencord/Vesktop</url>
<categories>
<category>InstantMessaging</category>
<category>AudioVideo</category>
</categories>
<requires>
<control>pointing</control>
<control>keyboard</control>
<display_length compare="ge">420</display_length>
<internet>always</internet>
</requires>
<recommends>
<control>voice</control>
<display_length compare="ge">760</display_length>
<display_length compare="le">1200</display_length>
<internet>always</internet>
</recommends>
<supports>
<internet>always</internet>
</supports>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
<content_attribute id="social-audio">intense</content_attribute>
<content_attribute id="social-contacts">intense</content_attribute>
<content_attribute id="social-info">intense</content_attribute>
</content_rating>
<keywords>
<keyword>Discord</keyword>
<keyword>Vencord</keyword>
<keyword>Vesktop</keyword>
<keyword>Privacy</keyword>
<keyword>Mod</keyword>
</keywords>
</component>

View File

@@ -1,20 +1,19 @@
{ {
"name": "vesktop", "name": "VencordDesktop",
"version": "1.6.1", "version": "0.4.4",
"private": true, "private": true,
"description": "Vesktop is a custom Discord desktop app", "description": "",
"keywords": [], "keywords": [],
"homepage": "https://vencord.dev/", "homepage": "https://vencord.dev/",
"license": "GPL-3.0-or-later", "license": "GPL-3.0",
"author": "Vendicated <vendicated@riseup.net>", "author": "Vendicated <vendicated@riseup.net>",
"main": "dist/js/main.js", "main": "dist/js/main.js",
"scripts": { "scripts": {
"build": "tsx scripts/build/build.mts", "build": "tsx scripts/build/build.mts",
"build:dev": "pnpm build --dev", "build:dev": "pnpm build --dev",
"buildLibVesktop": "pnpm -C packages/libvesktop install && pnpm -C packages/libvesktop run build",
"package": "pnpm build && electron-builder", "package": "pnpm build && electron-builder",
"package:dir": "pnpm build && electron-builder --dir", "package:dir": "pnpm build && electron-builder --dir",
"lint": "eslint", "lint": "eslint . --ext .js,.jsx,.ts,.tsx,.mts,.mjs",
"lint:fix": "pnpm lint --fix", "lint:fix": "pnpm lint --fix",
"start": "pnpm build && electron .", "start": "pnpm build && electron .",
"start:dev": "pnpm build:dev && electron .", "start:dev": "pnpm build:dev && electron .",
@@ -22,71 +21,58 @@
"test": "pnpm lint && pnpm testTypes", "test": "pnpm lint && pnpm testTypes",
"testTypes": "tsc --noEmit", "testTypes": "tsc --noEmit",
"watch": "pnpm build --watch", "watch": "pnpm build --watch",
"generateMeta": "tsx scripts/utils/generateMeta.mts", "updateMeta": "tsx scripts/utils/updateMeta.mts"
"updateArrpcDB": "node ./node_modules/arrpc/update_db.js",
"postinstall": "pnpm updateArrpcDB"
}, },
"dependencies": { "dependencies": {
"arrpc": "github:OpenAsar/arrpc#2234e9c9111f4c42ebcc3aa6a2215bfd979eef77", "arrpc": "github:OpenAsar/arrpc#3e22fd776273afaa4a80c51deb86077ffdd4d2ae"
"electron-updater": "^6.6.2"
}, },
"optionalDependencies": { "optionalDependencies": {
"@vencord/venmic": "^6.1.0" "@vencord/venmic": "^2.1.3"
}, },
"devDependencies": { "devDependencies": {
"@fal-works/esbuild-plugin-global-externals": "^2.1.2", "@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@stylistic/eslint-plugin": "^5.6.1", "@types/node": "^20.10.0",
"@types/node": "^25.0.1", "@types/react": "^18.2.39",
"@types/react": "19.2.1", "@typescript-eslint/eslint-plugin": "^6.13.1",
"@vencord/types": "^1.13.7", "@typescript-eslint/parser": "^6.13.1",
"dotenv": "^17.2.3", "@vencord/types": "^0.1.2",
"electron": "^39.2.7", "dotenv": "^16.3.1",
"electron-builder": "^26.0.12", "electron": "^27.1.2",
"esbuild": "^0.27.1", "electron-builder": "^24.9.1",
"eslint": "^9.39.1", "esbuild": "^0.19.8",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-path-alias": "^2.1.0", "eslint-plugin-license-header": "^0.6.0",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-path-alias": "^1.0.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-simple-header": "^1.2.2", "eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unused-imports": "^3.0.0",
"eslint-plugin-unused-imports": "^4.3.0", "prettier": "^3.1.0",
"libvesktop": "link:packages/libvesktop",
"prettier": "^3.7.4",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"tsx": "^4.21.0", "tsx": "^4.6.0",
"type-fest": "^5.3.1", "type-fest": "^4.8.2",
"typescript": "^5.9.3", "typescript": "^5.3.2",
"typescript-eslint": "^8.49.0", "xml-formatter": "^3.6.0"
"xml-formatter": "^3.6.7"
}, },
"packageManager": "pnpm@10.7.1", "packageManager": "pnpm@8.11.0",
"engines": { "engines": {
"node": ">=18", "node": ">=18",
"pnpm": ">=8" "pnpm": ">=8"
}, },
"build": { "build": {
"appId": "dev.vencord.vesktop", "appId": "dev.vencord.desktop",
"productName": "Vesktop", "productName": "Vesktop",
"executableName": "vesktop",
"files": [ "files": [
"!*", "!*",
"!node_modules",
"dist/js", "dist/js",
"static", "static",
"package.json", "package.json",
"LICENSE" "LICENSE"
], ],
"protocols": { "beforePack": "scripts/build/sandboxFix.js",
"name": "Discord",
"schemes": [
"discord"
]
},
"beforePack": "scripts/build/beforePack.mjs",
"afterPack": "scripts/build/afterPack.mjs",
"linux": { "linux": {
"icon": "build/icon.svg", "icon": "build/icon.icns",
"category": "Network", "category": "Network",
"maintainer": "vendicated+vesktop@riseup.net", "maintainer": "vendicated+vesktop@riseup.net",
"target": [ "target": [
@@ -120,15 +106,12 @@
} }
], ],
"desktop": { "desktop": {
"entry": { "Name": "Vesktop",
"Name": "Vesktop", "GenericName": "Internet Messenger",
"GenericName": "Internet Messenger", "Type": "Application",
"Type": "Application", "Categories": "Network;InstantMessaging;Chat;",
"Categories": "Network;InstantMessaging;Chat;", "Keywords": "discord;vencord;electron;chat;",
"Keywords": "discord;vencord;electron;chat;", "StartupWMClass": "VencordDesktop"
"MimeType": "x-scheme-handler/discord",
"StartupWMClass": "vesktop"
}
} }
}, },
"mac": { "mac": {
@@ -136,93 +119,31 @@
{ {
"target": "default", "target": "default",
"arch": [ "arch": [
"universal" "x64",
"arm64"
] ]
} }
], ],
"category": "public.app-category.social-networking", "category": "Network",
"darkModeSupport": true,
"extendInfo": { "extendInfo": {
"NSMicrophoneUsageDescription": "This app needs access to the microphone", "NSMicrophoneUsageDescription": "This app needs access to the microphone",
"NSCameraUsageDescription": "This app needs access to the camera", "NSCameraUsageDescription": "This app needs access to the camera",
"com.apple.security.device.audio-input": true, "com.apple.security.device.audio-input": true,
"com.apple.security.device.camera": true, "com.apple.security.device.camera": true
"CFBundleIconName": "Icon" }
},
"notarize": true
},
"dmg": {
"background": "build/background.tiff",
"icon": "build/icon.icns",
"iconSize": 105,
"window": {
"width": 512,
"height": 340
},
"contents": [
{
"x": 140,
"y": 160
},
{
"x": 372,
"y": 160,
"type": "link",
"path": "/Applications"
}
]
}, },
"nsis": { "nsis": {
"include": "build/installer.nsh", "include": "build/installer.nsh",
"oneClick": false "oneClick": false
}, },
"win": { "win": {
"icon": "build/icon.ico",
"target": [ "target": [
{ "nsis",
"target": "nsis", "zip"
"arch": [
"x64",
"arm64"
]
},
{
"target": "zip",
"arch": [
"x64",
"arm64"
]
}
] ]
}, },
"publish": { "publish": {
"provider": "github" "provider": "github"
},
"rpm": {
"fpm": [
"--rpm-rpmbuild-define=_build_id_links none"
]
},
"electronFuses": {
"runAsNode": false,
"enableCookieEncryption": false,
"enableNodeOptionsEnvironmentVariable": false,
"enableNodeCliInspectArguments": false,
"enableEmbeddedAsarIntegrityValidation": false,
"onlyLoadAppFromAsar": true,
"loadBrowserProcessSpecificV8Snapshot": false,
"grantFileProtocolExtraPrivileges": false
} }
},
"pnpm": {
"patchedDependencies": {
"arrpc@3.5.0": "patches/arrpc@3.5.0.patch",
"electron-updater": "patches/electron-updater.patch"
},
"onlyBuiltDependencies": [
"@vencord/venmic",
"electron",
"esbuild"
]
} }
} }

View File

@@ -1 +0,0 @@
build

View File

@@ -1,19 +0,0 @@
# Dockerfile for building both x64 and arm64 on an old distro for maximum compatibility.
# ubuntu20 is dead but debian11 is still supported for now
FROM debian:11
ENV DEBIAN_FRONTEND=noninteractive
RUN dpkg --add-architecture arm64
RUN apt-get update && apt-get install -y \
build-essential python3 curl pkg-config \
g++-aarch64-linux-gnu libglib2.0-dev:amd64 libglib2.0-dev:arm64 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get update && apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src

View File

@@ -1,19 +0,0 @@
{
"targets": [
{
"target_name": "libvesktop",
"sources": [ "src/libvesktop.cc" ],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"cflags_cc": [
"<!(pkg-config --cflags glib-2.0 gio-2.0)",
"-O3"
],
"libraries": [
"<!@(pkg-config --libs-only-l --libs-only-other glib-2.0 gio-2.0)"
],
"cflags_cc!": ["-fno-exceptions"],
}
]
}

View File

@@ -1,17 +0,0 @@
#!/bin/sh
set -e
docker build -t libvesktop-builder -f Dockerfile .
docker run --rm -v "$PWD":/src -w /src libvesktop-builder bash -c "
set -e
echo '=== Building x64 ==='
npx node-gyp rebuild --arch=x64
mv build/Release/vesktop.node prebuilds/vesktop-x64.node
echo '=== Building arm64 ==='
export CXX=aarch64-linux-gnu-g++
npx node-gyp rebuild --arch=arm64
mv build/Release/vesktop.node prebuilds/vesktop-arm64.node
"

View File

@@ -1,3 +0,0 @@
export function getAccentColor(): number | null;
export function requestBackground(autoStart: boolean, commandLine: string[]): boolean;
export function updateUnityLauncherCount(count: number): boolean;

View File

@@ -1,14 +0,0 @@
{
"name": "libvesktop",
"main": "build/Release/vesktop.node",
"types": "index.d.ts",
"devDependencies": {
"node-addon-api": "^8.5.0",
"node-gyp": "^11.4.2"
},
"scripts": {
"build": "node-gyp configure build",
"clean": "node-gyp clean",
"test": "npm run build && node test.js"
}
}

View File

@@ -1,730 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
node-addon-api:
specifier: ^8.5.0
version: 8.5.0
node-gyp:
specifier: ^11.4.2
version: 11.4.2
packages:
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
'@npmcli/agent@3.0.0':
resolution: {integrity: sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==}
engines: {node: ^18.17.0 || >=20.5.0}
'@npmcli/fs@4.0.0':
resolution: {integrity: sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==}
engines: {node: ^18.17.0 || >=20.5.0}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
abbrev@3.0.1:
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
engines: {node: ^18.17.0 || >=20.5.0}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
cacache@19.0.1:
resolution: {integrity: sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==}
engines: {node: ^18.17.0 || >=20.5.0}
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
encoding@0.1.13:
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
env-paths@2.2.1:
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
engines: {node: '>=6'}
err-code@2.0.3:
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
exponential-backoff@3.1.2:
resolution: {integrity: sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
fs-minipass@3.0.3:
resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
ip-address@10.0.1:
resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
engines: {node: '>= 12'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
isexe@3.1.1:
resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==}
engines: {node: '>=16'}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
make-fetch-happen@14.0.3:
resolution: {integrity: sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==}
engines: {node: ^18.17.0 || >=20.5.0}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
minipass-collect@2.0.1:
resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==}
engines: {node: '>=16 || 14 >=14.17'}
minipass-fetch@4.0.1:
resolution: {integrity: sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==}
engines: {node: ^18.17.0 || >=20.5.0}
minipass-flush@1.0.5:
resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==}
engines: {node: '>= 8'}
minipass-pipeline@1.2.4:
resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==}
engines: {node: '>=8'}
minipass-sized@1.0.3:
resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==}
engines: {node: '>=8'}
minipass@3.3.6:
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
minizlib@3.1.0:
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
node-addon-api@8.5.0:
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
engines: {node: ^18 || ^20 || >= 21}
node-gyp@11.4.2:
resolution: {integrity: sha512-3gD+6zsrLQH7DyYOUIutaauuXrcyxeTPyQuZQCQoNPZMHMMS5m4y0xclNpvYzoK3VNzuyxT6eF4mkIL4WSZ1eQ==}
engines: {node: ^18.17.0 || >=20.5.0}
hasBin: true
nopt@8.1.0:
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
engines: {node: ^18.17.0 || >=20.5.0}
hasBin: true
p-map@7.0.3:
resolution: {integrity: sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==}
engines: {node: '>=18'}
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
proc-log@5.0.0:
resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
engines: {node: ^18.17.0 || >=20.5.0}
promise-retry@2.0.1:
resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
engines: {node: '>=10'}
retry@0.12.0:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
hasBin: true
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
socks-proxy-agent@8.0.5:
resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==}
engines: {node: '>= 14'}
socks@2.8.7:
resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
ssri@12.0.0:
resolution: {integrity: sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==}
engines: {node: ^18.17.0 || >=20.5.0}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@5.1.2:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.2:
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
engines: {node: '>=12'}
tar@7.5.1:
resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==}
engines: {node: '>=18'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
unique-filename@4.0.0:
resolution: {integrity: sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==}
engines: {node: ^18.17.0 || >=20.5.0}
unique-slug@5.0.0:
resolution: {integrity: sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==}
engines: {node: ^18.17.0 || >=20.5.0}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
which@5.0.0:
resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==}
engines: {node: ^18.17.0 || >=20.5.0}
hasBin: true
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@8.1.0:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
snapshots:
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.2
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
'@npmcli/agent@3.0.0':
dependencies:
agent-base: 7.1.4
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
lru-cache: 10.4.3
socks-proxy-agent: 8.0.5
transitivePeerDependencies:
- supports-color
'@npmcli/fs@4.0.0':
dependencies:
semver: 7.7.2
'@pkgjs/parseargs@0.11.0':
optional: true
abbrev@3.0.1: {}
agent-base@7.1.4: {}
ansi-regex@5.0.1: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.3: {}
balanced-match@1.0.2: {}
brace-expansion@2.0.2:
dependencies:
balanced-match: 1.0.2
cacache@19.0.1:
dependencies:
'@npmcli/fs': 4.0.0
fs-minipass: 3.0.3
glob: 10.4.5
lru-cache: 10.4.3
minipass: 7.1.2
minipass-collect: 2.0.1
minipass-flush: 1.0.5
minipass-pipeline: 1.2.4
p-map: 7.0.3
ssri: 12.0.0
tar: 7.5.1
unique-filename: 4.0.0
chownr@3.0.0: {}
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
debug@4.4.3:
dependencies:
ms: 2.1.3
eastasianwidth@0.2.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
encoding@0.1.13:
dependencies:
iconv-lite: 0.6.3
optional: true
env-paths@2.2.1: {}
err-code@2.0.3: {}
exponential-backoff@3.1.2: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
fs-minipass@3.0.3:
dependencies:
minipass: 7.1.2
glob@10.4.5:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.5
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
graceful-fs@4.2.11: {}
http-cache-semantics@4.2.0: {}
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
transitivePeerDependencies:
- supports-color
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2
optional: true
imurmurhash@0.1.4: {}
ip-address@10.0.1: {}
is-fullwidth-code-point@3.0.0: {}
isexe@2.0.0: {}
isexe@3.1.1: {}
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
lru-cache@10.4.3: {}
make-fetch-happen@14.0.3:
dependencies:
'@npmcli/agent': 3.0.0
cacache: 19.0.1
http-cache-semantics: 4.2.0
minipass: 7.1.2
minipass-fetch: 4.0.1
minipass-flush: 1.0.5
minipass-pipeline: 1.2.4
negotiator: 1.0.0
proc-log: 5.0.0
promise-retry: 2.0.1
ssri: 12.0.0
transitivePeerDependencies:
- supports-color
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
minipass-collect@2.0.1:
dependencies:
minipass: 7.1.2
minipass-fetch@4.0.1:
dependencies:
minipass: 7.1.2
minipass-sized: 1.0.3
minizlib: 3.1.0
optionalDependencies:
encoding: 0.1.13
minipass-flush@1.0.5:
dependencies:
minipass: 3.3.6
minipass-pipeline@1.2.4:
dependencies:
minipass: 3.3.6
minipass-sized@1.0.3:
dependencies:
minipass: 3.3.6
minipass@3.3.6:
dependencies:
yallist: 4.0.0
minipass@7.1.2: {}
minizlib@3.1.0:
dependencies:
minipass: 7.1.2
ms@2.1.3: {}
negotiator@1.0.0: {}
node-addon-api@8.5.0: {}
node-gyp@11.4.2:
dependencies:
env-paths: 2.2.1
exponential-backoff: 3.1.2
graceful-fs: 4.2.11
make-fetch-happen: 14.0.3
nopt: 8.1.0
proc-log: 5.0.0
semver: 7.7.2
tar: 7.5.1
tinyglobby: 0.2.15
which: 5.0.0
transitivePeerDependencies:
- supports-color
nopt@8.1.0:
dependencies:
abbrev: 3.0.1
p-map@7.0.3: {}
package-json-from-dist@1.0.1: {}
path-key@3.1.1: {}
path-scurry@1.11.1:
dependencies:
lru-cache: 10.4.3
minipass: 7.1.2
picomatch@4.0.3: {}
proc-log@5.0.0: {}
promise-retry@2.0.1:
dependencies:
err-code: 2.0.3
retry: 0.12.0
retry@0.12.0: {}
safer-buffer@2.1.2:
optional: true
semver@7.7.2: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
signal-exit@4.1.0: {}
smart-buffer@4.2.0: {}
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.4
debug: 4.4.3
socks: 2.8.7
transitivePeerDependencies:
- supports-color
socks@2.8.7:
dependencies:
ip-address: 10.0.1
smart-buffer: 4.2.0
ssri@12.0.0:
dependencies:
minipass: 7.1.2
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@5.1.2:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.2
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.2:
dependencies:
ansi-regex: 6.2.2
tar@7.5.1:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
minipass: 7.1.2
minizlib: 3.1.0
yallist: 5.0.0
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
unique-filename@4.0.0:
dependencies:
unique-slug: 5.0.0
unique-slug@5.0.0:
dependencies:
imurmurhash: 0.1.4
which@2.0.2:
dependencies:
isexe: 2.0.0
which@5.0.0:
dependencies:
isexe: 3.1.1
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.3
string-width: 5.1.2
strip-ansi: 7.1.2
yallist@4.0.0: {}
yallist@5.0.0: {}

View File

@@ -1,269 +0,0 @@
#include <gio/gio.h>
#include <cstdlib>
#include <cstdint>
#include <iostream>
#include <napi.h>
#include <optional>
#include <cmath>
#include <memory>
template <typename T>
struct GObjectDeleter
{
void operator()(T *obj) const
{
if (obj)
g_object_unref(obj);
}
};
template <typename T>
using GObjectPtr = std::unique_ptr<T, GObjectDeleter<T>>;
struct GVariantDeleter
{
void operator()(GVariant *variant) const
{
if (variant)
g_variant_unref(variant);
}
};
using GVariantPtr = std::unique_ptr<GVariant, GVariantDeleter>;
struct GErrorDeleter
{
void operator()(GError *error) const
{
if (error)
g_error_free(error);
}
};
using GErrorPtr = std::unique_ptr<GError, GErrorDeleter>;
bool update_launcher_count(int count)
{
GError *error = nullptr;
const char *chromeDesktop = std::getenv("CHROME_DESKTOP");
std::string desktop_id = std::string("application://") + (chromeDesktop ? chromeDesktop : "vesktop.desktop");
GObjectPtr<GDBusConnection> bus(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error));
if (!bus)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::update_launcher_count] Failed to connect to session bus: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return false;
}
GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&builder, "{sv}", "count", g_variant_new_int64(count));
g_variant_builder_add(&builder, "{sv}", "count-visible", g_variant_new_boolean(count != 0));
gboolean result = g_dbus_connection_emit_signal(
bus.get(),
nullptr,
"/",
"com.canonical.Unity.LauncherEntry",
"Update",
g_variant_new("(sa{sv})", desktop_id.c_str(), &builder),
&error);
if (!result || error)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::update_launcher_count] Failed to emit Update signal: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return false;
}
return true;
}
std::optional<int32_t> get_accent_color()
{
GError *error = nullptr;
GObjectPtr<GDBusConnection> bus(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error));
if (!bus)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::get_accent_color] Failed to connect to session bus: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return std::nullopt;
}
GVariantPtr reply(g_dbus_connection_call_sync(
bus.get(),
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Settings",
"Read",
g_variant_new("(ss)", "org.freedesktop.appearance", "accent-color"),
nullptr,
G_DBUS_CALL_FLAGS_NONE,
5000,
nullptr,
&error));
if (!reply)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::get_accent_color] Failed to call Read: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return std::nullopt;
}
GVariant *inner_raw = nullptr;
g_variant_get(reply.get(), "(v)", &inner_raw);
if (!inner_raw)
{
std::cerr << "[libvesktop::get_accent_color] Inner variant is null" << std::endl;
return std::nullopt;
}
GVariantPtr inner(inner_raw);
// Unwrap nested variants
while (g_variant_is_of_type(inner.get(), G_VARIANT_TYPE_VARIANT))
{
GVariant *next = g_variant_get_variant(inner.get());
inner.reset(next);
}
if (!g_variant_is_of_type(inner.get(), G_VARIANT_TYPE_TUPLE) ||
g_variant_n_children(inner.get()) < 3)
{
std::cerr << "[libvesktop::get_accent_color] Inner variant is not a tuple of 3 doubles" << std::endl;
return std::nullopt;
}
double r = 0.0, g = 0.0, b = 0.0;
g_variant_get(inner.get(), "(ddd)", &r, &g, &b);
bool discard = false;
auto toInt = [&discard](double v) -> int
{
if (!std::isfinite(v) || v < 0.0 || v > 1.0)
{
discard = true;
return 0;
}
return static_cast<int>(std::round(v * 255.0));
};
int32_t rgb = (toInt(r) << 16) | (toInt(g) << 8) | toInt(b);
if (discard)
return std::nullopt;
return rgb;
}
bool request_background(bool autostart, const std::vector<std::string> &commandline)
{
GError *error = nullptr;
GObjectPtr<GDBusConnection> bus(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error));
if (!bus)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::request_background] Failed to connect to session bus: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return false;
}
GVariantBuilder builder;
g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&builder, "{sv}", "autostart", g_variant_new_boolean(autostart));
if (!commandline.empty())
{
GVariantBuilder cmd_builder;
g_variant_builder_init(&cmd_builder, G_VARIANT_TYPE("as"));
for (const auto &s : commandline)
g_variant_builder_add(&cmd_builder, "s", s.c_str());
g_variant_builder_add(&builder, "{sv}", "commandline", g_variant_builder_end(&cmd_builder));
}
GVariantPtr reply(g_dbus_connection_call_sync(
bus.get(),
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Background",
"RequestBackground",
g_variant_new("(sa{sv})", "", &builder),
nullptr,
G_DBUS_CALL_FLAGS_NONE,
5000,
nullptr,
&error));
if (!reply)
{
GErrorPtr error_ptr(error);
std::cerr << "[libvesktop::request_background] Failed to call RequestBackground: "
<< (error_ptr ? error_ptr->message : "unknown error") << std::endl;
return false;
}
return true;
}
Napi::Value updateUnityLauncherCount(Napi::CallbackInfo const &info)
{
if (info.Length() < 1 || !info[0].IsNumber())
{
Napi::TypeError::New(info.Env(), "Expected (number)").ThrowAsJavaScriptException();
return info.Env().Undefined();
}
int count = info[0].As<Napi::Number>().Int32Value();
bool success = update_launcher_count(count);
return Napi::Boolean::New(info.Env(), success);
}
Napi::Value getAccentColor(const Napi::CallbackInfo &info)
{
auto color = get_accent_color();
if (color)
return Napi::Number::New(info.Env(), *color);
return info.Env().Null();
}
Napi::Value RequestBackground(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsBoolean() || !info[1].IsArray())
{
Napi::TypeError::New(env, "Expected (boolean, string[])").ThrowAsJavaScriptException();
return env.Null();
}
bool autostart = info[0].As<Napi::Boolean>();
Napi::Array arr = info[1].As<Napi::Array>();
std::vector<std::string> commandline;
for (uint32_t i = 0; i < arr.Length(); i++)
{
Napi::Value v = arr.Get(i);
if (v.IsString())
commandline.push_back(v.As<Napi::String>().Utf8Value());
}
bool ok = request_background(autostart, commandline);
return Napi::Boolean::New(env, ok);
}
Napi::Object Init(Napi::Env env, Napi::Object exports)
{
exports.Set("updateUnityLauncherCount", Napi::Function::New(env, updateUnityLauncherCount));
exports.Set("getAccentColor", Napi::Function::New(env, getAccentColor));
exports.Set("requestBackground", Napi::Function::New(env, RequestBackground));
return exports;
}
NODE_API_MODULE(libvesktop, Init)

View File

@@ -1,22 +0,0 @@
/**
* @type {typeof import(".")}
*/
const libVesktop = require(".");
const test = require("node:test");
const assert = require("node:assert/strict");
test("getAccentColor should return a number", () => {
const color = libVesktop.getAccentColor();
assert.strictEqual(typeof color, "number");
});
test("updateUnityLauncherCount should return true (success)", () => {
assert.strictEqual(libVesktop.updateUnityLauncherCount(5), true);
assert.strictEqual(libVesktop.updateUnityLauncherCount(0), true);
assert.strictEqual(libVesktop.updateUnityLauncherCount(10), true);
});
test("requestBackground should return true (success)", () => {
assert.strictEqual(libVesktop.requestBackground(true, ["bash"]), true);
assert.strictEqual(libVesktop.requestBackground(false, []), true);
});

View File

@@ -1,27 +0,0 @@
diff --git a/src/process/index.js b/src/process/index.js
index 389b0845256a34b4536d6da99edb00d17f13a6b4..f17a0ac687e9110ebfd33cb91fd2f6250d318643 100644
--- a/src/process/index.js
+++ b/src/process/index.js
@@ -5,8 +5,20 @@ import fs from 'node:fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const DetectableDB = JSON.parse(fs.readFileSync(join(__dirname, 'detectable.json'), 'utf8'));
+const DetectableDB = require('./detectable.json');
+DetectableDB.push(
+ {
+ aliases: ["Obs"],
+ executables: [
+ { is_launcher: false, name: "obs", os: "linux" },
+ { is_launcher: false, name: "obs.exe", os: "win32" },
+ { is_launcher: false, name: "obs.app", os: "darwin" }
+ ],
+ hook: true,
+ id: "STREAMERMODE",
+ name: "OBS"
+ }
+);
import * as Natives from './native/index.js';
const Native = Natives[process.platform];

View File

@@ -1,16 +0,0 @@
diff --git a/out/RpmUpdater.js b/out/RpmUpdater.js
index 563187bb18cb0bd154dff6620cb62b8c8f534cd6..d91594026c2bac9cc78ef3b1183df3241d7d9624 100644
--- a/out/RpmUpdater.js
+++ b/out/RpmUpdater.js
@@ -32,7 +32,10 @@ class RpmUpdater extends BaseUpdater_1.BaseUpdater {
const sudo = this.wrapSudo();
// pkexec doesn't want the command to be wrapped in " quotes
const wrapper = /pkexec/i.test(sudo) ? "" : `"`;
- const packageManager = this.spawnSyncLog("which zypper");
+ let packageManager;
+ try {
+ packageManager = this.spawnSyncLog("which zypper");
+ } catch {}
const installerPath = this.installerPath;
if (installerPath == null) {
this.dispatchError(new Error("No valid update available, can't quit and install"));

8725
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
import { copyFile, readdir } from "fs/promises";
/**
* @param {{
* readonly appOutDir: string;
* readonly arch: Arch;
* readonly electronPlatformName: string;
* readonly outDir: string;
* readonly packager: PlatformPackager;
* readonly targets: Target[];
* }} context
*/
export async function addAssetsCar({ appOutDir }) {
if (process.platform !== "darwin") return;
const appName = (await readdir(appOutDir)).find(item => item.endsWith(".app"));
if (!appName) {
console.warn(`Could not find .app directory in ${appOutDir}. Skipping adding assets.car`);
return;
}
await copyFile("build/Assets.car", `${appOutDir}/${appName}/Contents/Resources/Assets.car`);
}

View File

@@ -1,5 +0,0 @@
import { addAssetsCar } from "./addAssetsCar.mjs";
export default async function afterPack(context) {
await addAssetsCar(context);
}

View File

@@ -1,5 +0,0 @@
import { applyAppImageSandboxFix } from "./sandboxFix.mjs";
export default async function beforePack() {
await applyAppImageSandboxFix();
}

View File

@@ -1,14 +1,13 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { BuildContext, BuildOptions, context } from "esbuild"; import { BuildContext, BuildOptions, context } from "esbuild";
import { copyFile } from "fs/promises"; import { copyFile } from "fs/promises";
import vencordDep from "./vencordDep.mjs"; import vencordDep from "./vencordDep.mjs";
import { includeDirPlugin } from "./includeDirPlugin.mts";
const isDev = process.argv.includes("--dev"); const isDev = process.argv.includes("--dev");
@@ -25,9 +24,6 @@ const NodeCommonOpts: BuildOptions = {
platform: "node", platform: "node",
external: ["electron"], external: ["electron"],
target: ["esnext"], target: ["esnext"],
loader: {
".node": "file"
},
define: { define: {
IS_DEV: JSON.stringify(isDev) IS_DEV: JSON.stringify(isDev)
} }
@@ -53,58 +49,25 @@ async function copyVenmic() {
]).catch(() => console.warn("Failed to copy venmic. Building without venmic support")); ]).catch(() => console.warn("Failed to copy venmic. Building without venmic support"));
} }
async function copyLibVesktop() {
if (process.platform !== "linux") return;
try {
await copyFile(
"./packages/libvesktop/build/Release/vesktop.node",
`./static/dist/libvesktop-${process.arch}.node`
);
console.log("Using local libvesktop build");
} catch {
console.log(
"Using prebuilt libvesktop binaries. Run `pnpm buildLibVesktop` and build again to build from source - see README.md for more details"
);
return Promise.all([
copyFile("./packages/libvesktop/prebuilds/vesktop-x64.node", "./static/dist/libvesktop-x64.node"),
copyFile("./packages/libvesktop/prebuilds/vesktop-arm64.node", "./static/dist/libvesktop-arm64.node")
]).catch(() => console.warn("Failed to copy libvesktop. Building without libvesktop support"));
}
}
await Promise.all([ await Promise.all([
copyVenmic(), copyVenmic(),
copyLibVesktop(),
createContext({ createContext({
...NodeCommonOpts, ...NodeCommonOpts,
entryPoints: ["src/main/index.ts"], entryPoints: ["src/main/index.ts"],
outfile: "dist/js/main.js", outfile: "dist/js/main.js",
footer: { js: "//# sourceURL=VesktopMain" } footer: { js: "//# sourceURL=VCDMain" }
}),
createContext({
...NodeCommonOpts,
entryPoints: ["src/main/arrpc/worker.ts"],
outfile: "dist/js/arRpcWorker.js",
footer: { js: "//# sourceURL=VesktopArRpcWorker" }
}), }),
createContext({ createContext({
...NodeCommonOpts, ...NodeCommonOpts,
entryPoints: ["src/preload/index.ts"], entryPoints: ["src/preload/index.ts"],
outfile: "dist/js/preload.js", outfile: "dist/js/preload.js",
footer: { js: "//# sourceURL=VesktopPreload" } footer: { js: "//# sourceURL=VCDPreload" }
}), }),
createContext({ createContext({
...NodeCommonOpts, ...NodeCommonOpts,
entryPoints: ["src/preload/splash.ts"], entryPoints: ["src/updater/preload.ts"],
outfile: "dist/js/splashPreload.js",
footer: { js: "//# sourceURL=VesktopSplashPreload" }
}),
createContext({
...NodeCommonOpts,
entryPoints: ["src/preload/updater.ts"],
outfile: "dist/js/updaterPreload.js", outfile: "dist/js/updaterPreload.js",
footer: { js: "//# sourceURL=VesktopUpdaterPreload" } footer: { js: "//# sourceURL=VCDUpdaterPreload" }
}), }),
createContext({ createContext({
...CommonOpts, ...CommonOpts,
@@ -116,8 +79,8 @@ await Promise.all([
jsxFactory: "VencordCreateElement", jsxFactory: "VencordCreateElement",
jsxFragment: "VencordFragment", jsxFragment: "VencordFragment",
external: ["@vencord/types/*"], external: ["@vencord/types/*"],
plugins: [vencordDep, includeDirPlugin("patches", "src/renderer/patches")], plugins: [vencordDep],
footer: { js: "//# sourceURL=VesktopRenderer" } footer: { js: "//# sourceURL=VCDRenderer" }
}) })
]); ]);

View File

@@ -1,25 +0,0 @@
import { Plugin } from "esbuild";
import { readdir } from "fs/promises";
const makeImportAllCode = (files: string[]) =>
files.map(f => `require("./${f.replace(/\.[cm]?[tj]sx?$/, "")}")`).join("\n");
const makeImportDirRecursiveCode = (dir: string) => readdir(dir).then(files => makeImportAllCode(files));
export function includeDirPlugin(namespace: string, path: string): Plugin {
return {
name: `include-dir-plugin:${namespace}`,
setup(build) {
const filter = new RegExp(`^__${namespace}__$`);
build.onResolve({ filter }, args => ({ path: args.path, namespace }));
build.onLoad({ filter, namespace }, async args => {
return {
contents: await makeImportDirRecursiveCode(path),
resolveDir: path
};
});
}
};
}

View File

@@ -1,3 +1,9 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment"); export const VencordFragment = /* #__PURE__*/ Symbol.for("react.fragment");
export let VencordCreateElement = (...args) => export let VencordCreateElement = (...args) =>
(VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args); (VencordCreateElement = Vencord.Webpack.Common.React.createElement)(...args);

View File

@@ -1,26 +1,23 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
// Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js // Based on https://github.com/gergof/electron-builder-sandbox-fix/blob/master/lib/index.js
import fs from "fs/promises"; const fs = require("fs/promises");
import path from "path"; const path = require("path");
import AppImageTarget from "app-builder-lib/out/targets/AppImageTarget.js";
let isApplied = false; let isApplied = false;
export async function applyAppImageSandboxFix() { const hook = async () => {
if (isApplied) return;
isApplied = true;
if (process.platform !== "linux") { if (process.platform !== "linux") {
// this fix is only required on linux // this fix is only required on linux
return; return;
} }
const AppImageTarget = require("app-builder-lib/out/targets/AppImageTarget");
if (isApplied) return;
isApplied = true;
const oldBuildMethod = AppImageTarget.default.prototype.build; const oldBuildMethod = AppImageTarget.default.prototype.build;
AppImageTarget.default.prototype.build = async function (...args) { AppImageTarget.default.prototype.build = async function (...args) {
console.log("Running AppImage builder hook", args); console.log("Running AppImage builder hook", args);
@@ -72,4 +69,6 @@ exec "$SCRIPT_DIR/${this.packager.executableName}.bin" "$([ "$IS_STEAMOS" == 1 ]
return ret; return ret;
}; };
} };
module.exports = hook;

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { globalExternalsWithRegExp } from "@fal-works/esbuild-plugin-global-externals"; import { globalExternalsWithRegExp } from "@fal-works/esbuild-plugin-global-externals";

View File

@@ -1,5 +1,5 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) {year} {author} * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */

View File

@@ -8,4 +8,4 @@ import "./utils/dotenv";
import { spawnNodeModuleBin } from "./utils/spawn.mjs"; import { spawnNodeModuleBin } from "./utils/spawn.mjs";
spawnNodeModuleBin("electron", [process.cwd(), ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]); spawnNodeModuleBin("electron", [".", ...(process.env.ELECTRON_LAUNCH_FLAGS?.split(" ") ?? [])]);

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import "./start"; import "./start";

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { config } from "dotenv"; import { config } from "dotenv";

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { spawn as spaaawn, SpawnOptions } from "child_process"; import { spawn as spaaawn, SpawnOptions } from "child_process";

View File

@@ -1,11 +1,11 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { promises as fs } from "node:fs"; import { promises as fs } from "node:fs";
import { mkdir } from "node:fs/promises";
import { DOMParser, XMLSerializer } from "@xmldom/xmldom"; import { DOMParser, XMLSerializer } from "@xmldom/xmldom";
import xmlFormat from "xml-formatter"; import xmlFormat from "xml-formatter";
@@ -43,25 +43,14 @@ function generateDescription(description: string, descriptionNode: Element) {
} }
} }
const releases = await fetch("https://api.github.com/repos/Vencord/Vesktop/releases", { const latestReleaseInformation = await fetch("https://api.github.com/repos/Vencord/Vesktop/releases/latest", {
headers: { headers: {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
"X-Github-Api-Version": "2022-11-28" "X-Github-Api-Version": "2022-11-28"
} }
}).then(res => res.json()); }).then(res => res.json());
const latestReleaseInformation = releases[0]; const metaInfo = await fs.readFile("./meta/dev.vencord.Vesktop.metainfo.xml", "utf-8");
const metaInfo = await (async () => {
for (const release of releases) {
const metaAsset = release.assets.find((a: any) => a.name === "dev.vencord.Vesktop.metainfo.xml");
if (metaAsset) return fetch(metaAsset.browser_download_url).then(res => res.text());
}
})();
if (!metaInfo) {
throw new Error("Could not find existing meta information from any release");
}
const parser = new DOMParser().parseFromString(metaInfo, "text/xml"); const parser = new DOMParser().parseFromString(metaInfo, "text/xml");
@@ -101,7 +90,4 @@ const output = xmlFormat(new XMLSerializer().serializeToString(parser), {
indentation: " " indentation: " "
}); });
await mkdir("./dist", { recursive: true }); await fs.writeFile("./meta/dev.vencord.Vesktop.metainfo.xml", output, "utf-8");
await fs.writeFile("./dist/dev.vencord.Vesktop.metainfo.xml", output, "utf-8");
console.log("Updated meta information written to ./dist/dev.vencord.Vesktop.metainfo.xml");

4
src/globals.d.ts vendored
View File

@@ -1,13 +1,13 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
declare global { declare global {
export var VesktopNative: typeof import("preload/VesktopNative").VesktopNative; export var VesktopNative: typeof import("preload/VesktopNative").VesktopNative;
export var Vesktop: typeof import("renderer/index"); export var Vesktop: typeof import("renderer/index");
export var VesktopPatchGlobals: any; export var VCDP: any;
export var IS_DEV: boolean; export var IS_DEV: boolean;
} }

View File

@@ -1,32 +1,28 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { app, BrowserWindow } from "electron"; import { BrowserWindow } from "electron";
import { join } from "path";
import { ICON_PATH, VIEW_DIR } from "shared/paths";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { loadView } from "./vesktopStatic";
export async function createAboutWindow() {
const height = 750;
const width = height * (4 / 3);
export function createAboutWindow() {
const about = new BrowserWindow({ const about = new BrowserWindow({
center: true, center: true,
autoHideMenuBar: true, autoHideMenuBar: true,
height, icon: ICON_PATH,
width webPreferences: {
preload: join(__dirname, "updaterPreload.js")
}
}); });
makeLinksOpenExternally(about); makeLinksOpenExternally(about);
const data = new URLSearchParams({ about.loadFile(join(VIEW_DIR, "about.html"));
APP_VERSION: app.getVersion()
});
loadView(about, "about.html", data);
return about; return about;
} }

View File

@@ -1,17 +1,13 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { app, NativeImage, nativeImage } from "electron"; import { app, NativeImage, nativeImage } from "electron";
import { join } from "path"; import { join } from "path";
import { BADGE_DIR } from "shared/paths"; import { BADGE_DIR } from "shared/paths";
import { updateUnityLauncherCount } from "./dbus";
import { AppEvents } from "./events";
import { mainWin } from "./mainWindow";
const imgCache = new Map<number, NativeImage>(); const imgCache = new Map<number, NativeImage>();
function loadBadge(index: number) { function loadBadge(index: number) {
const cached = imgCache.get(index); const cached = imgCache.get(index);
@@ -25,24 +21,18 @@ function loadBadge(index: number) {
let lastIndex: null | number = -1; let lastIndex: null | number = -1;
/**
* -1 = show unread indicator
* 0 = clear
*/
export function setBadgeCount(count: number) { export function setBadgeCount(count: number) {
AppEvents.emit("setTrayVariant", count !== 0 ? "trayUnread" : "tray");
switch (process.platform) { switch (process.platform) {
case "linux": case "linux":
if (count === -1) count = 0; if (count === -1) count = 0;
updateUnityLauncherCount(count); app.setBadgeCount(count);
break; break;
case "darwin": case "darwin":
if (count === 0) { if (count === 0) {
app.dock!.setBadge(""); app.dock.setBadge("");
break; break;
} }
app.dock!.setBadge(count === -1 ? "•" : count.toString()); app.dock.setBadge(count === -1 ? "•" : count.toString());
break; break;
case "win32": case "win32":
const [index, description] = getBadgeIndexAndDescription(count); const [index, description] = getBadgeIndexAndDescription(count);
@@ -50,6 +40,8 @@ export function setBadgeCount(count: number) {
lastIndex = index; lastIndex = index;
// circular import shenanigans
const { mainWin } = require("./mainWindow") as typeof import("./mainWindow");
mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description); mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description);
break; break;
} }

38
src/main/arrpc.ts Normal file
View File

@@ -0,0 +1,38 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import Server from "arrpc";
import { IpcEvents } from "shared/IpcEvents";
import { mainWin } from "./mainWindow";
import { Settings } from "./settings";
let server: any;
const inviteCodeRegex = /^(\w|-)+$/;
export async function initArRPC() {
if (server || !Settings.store.arRPC) return;
try {
server = await new Server();
server.on("activity", (data: any) => mainWin.webContents.send(IpcEvents.ARRPC_ACTIVITY, JSON.stringify(data)));
server.on("invite", (invite: string, callback: (valid: boolean) => void) => {
invite = String(invite);
if (!inviteCodeRegex.test(invite)) return callback(false);
mainWin.webContents
// Safety: Result of JSON.stringify should always be safe to equal
// Also, just to be super super safe, invite is regex validated above
.executeJavaScript(`Vesktop.openInviteModal(${JSON.stringify(invite)})`)
.then(callback);
});
} catch (e) {
console.error("Failed to start arRPC server", e);
}
}
Settings.addChangeListener("arRPC", initArRPC);

View File

@@ -1,77 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { resolve } from "path";
import { IpcCommands } from "shared/IpcEvents";
import { MessageChannel, Worker } from "worker_threads";
import { sendRendererCommand } from "../ipcCommands";
import { Settings } from "../settings";
import { ArRpcEvent, ArRpcHostEvent } from "./types";
let worker: Worker;
const inviteCodeRegex = /^(\w|-)+$/;
export async function initArRPC() {
if (worker || !Settings.store.arRPC) return;
try {
const { port1: hostPort, port2: workerPort } = new MessageChannel();
worker = new Worker(resolve(__dirname, "./arRpcWorker.js"), {
workerData: {
workerPort
},
transferList: [workerPort]
});
hostPort.on("message", async ({ type, nonce, data }: ArRpcEvent) => {
switch (type) {
case "activity": {
sendRendererCommand(IpcCommands.RPC_ACTIVITY, data);
break;
}
case "invite": {
const invite = String(data);
const response: ArRpcHostEvent = {
type: "ack-invite",
nonce,
data: false
};
if (!inviteCodeRegex.test(invite)) {
return hostPort.postMessage(response);
}
response.data = await sendRendererCommand(IpcCommands.RPC_INVITE, invite).catch(() => false);
hostPort.postMessage(response);
break;
}
case "link": {
const response: ArRpcHostEvent = {
type: "ack-link",
nonce: nonce,
data: false
};
response.data = await sendRendererCommand(IpcCommands.RPC_DEEP_LINK, data).catch(() => false);
hostPort.postMessage(response);
break;
}
}
});
} catch (e) {
console.error("Failed to start arRPC server", e);
}
}
Settings.addChangeListener("arRPC", initArRPC);

View File

@@ -1,38 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export type ArRpcEvent = ArRpcActivityEvent | ArRpcInviteEvent | ArRpcLinkEvent;
export type ArRpcHostEvent = ArRpcHostAckInviteEvent | ArRpcHostAckLinkEvent;
export interface ArRpcActivityEvent {
type: "activity";
nonce: string;
data: string;
}
export interface ArRpcInviteEvent {
type: "invite";
nonce: string;
data: string;
}
export interface ArRpcLinkEvent {
type: "link";
nonce: string;
data: any;
}
export interface ArRpcHostAckInviteEvent {
type: "ack-invite";
nonce: string;
data: boolean;
}
export interface ArRpcHostAckLinkEvent {
type: "ack-link";
nonce: string;
data: boolean;
}

View File

@@ -1,73 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import Server from "arrpc";
import { randomUUID } from "crypto";
import { MessagePort, workerData } from "worker_threads";
import { ArRpcEvent, ArRpcHostEvent } from "./types";
let server: any;
type InviteCallback = (valid: boolean) => void;
type LinkCallback = InviteCallback;
const inviteCallbacks = new Map<string, InviteCallback>();
const linkCallbacks = new Map<string, LinkCallback>();
(async function () {
const { workerPort } = workerData as { workerPort: MessagePort };
server = await new Server();
server.on("activity", (data: any) => {
const event: ArRpcEvent = {
type: "activity",
data: JSON.stringify(data),
nonce: randomUUID()
};
workerPort.postMessage(event);
});
server.on("invite", (invite: string, callback: InviteCallback) => {
const nonce = randomUUID();
inviteCallbacks.set(nonce, callback);
const event: ArRpcEvent = {
type: "invite",
data: invite,
nonce
};
workerPort.postMessage(event);
});
server.on("link", async (data: any, callback: LinkCallback) => {
const nonce = randomUUID();
linkCallbacks.set(nonce, callback);
const event: ArRpcEvent = {
type: "link",
data,
nonce
};
workerPort.postMessage(event);
});
workerPort.on("message", (e: ArRpcHostEvent) => {
switch (e.type) {
case "ack-invite": {
inviteCallbacks.get(e.nonce)?.(e.data);
inviteCallbacks.delete(e.nonce);
break;
}
case "ack-link": {
linkCallbacks.get(e.nonce)?.(e.data);
linkCallbacks.delete(e.nonce);
break;
}
}
});
})();

View File

@@ -1,100 +1,62 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { app } from "electron"; import { app } from "electron";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"; import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
import { join } from "path"; import { join } from "path";
import { stripIndent } from "shared/utils/text";
import { IS_FLATPAK } from "./constants";
import { requestBackground } from "./dbus";
import { Settings, State } from "./settings";
import { escapeDesktopFileArgument } from "./utils/desktopFileEscape";
interface AutoStart { interface AutoStart {
isEnabled(): boolean; isEnabled(): boolean;
wasAutoStarted(): boolean;
enable(): void; enable(): void;
disable(): void; disable(): void;
} }
function getEscapedCommandLine() { const isFlatpak = process.env.FLATPAK_ID !== undefined;
const args = process.argv.map(escapeDesktopFileArgument);
if (Settings.store.autoStartMinimized) args.push("--start-minimized");
return args;
}
function makeAutoStartLinuxDesktop(): AutoStart { function makeAutoStartLinux(): AutoStart {
const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config"); const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config");
const dir = join(configDir, "autostart"); const dir = join(configDir, "autostart");
const file = join(dir, "vesktop.desktop"); const file = join(dir, "vencord.desktop");
return { return {
isEnabled: () => existsSync(file), isEnabled: () => existsSync(file), // TODO: flatpak
wasAutoStarted: () => process.argv.includes("--autostart"),
enable() { enable() {
const desktopFile = stripIndent` if (isFlatpak) {
[Desktop Entry] } else {
Type=Application const desktopFile = `
Name=Vesktop [Desktop Entry]
Comment=Vesktop autostart script Type=Application
Exec=${getEscapedCommandLine().join(" ")} Version=1.0
StartupNotify=false Name=Vencord
Terminal=false Comment=Vencord autostart script
Icon=vesktop Exec=${process.execPath} --autostart
`; Terminal=false
StartupNotify=false
`.trim();
mkdirSync(dir, { recursive: true }); mkdirSync(dir, { recursive: true });
writeFileSync(file, desktopFile); writeFileSync(file, desktopFile);
},
disable: () => rmSync(file, { force: true })
};
}
function makeAutoStartLinuxPortal() {
return {
isEnabled: () => State.store.linuxAutoStartEnabled === true,
enable() {
const success = requestBackground(true, getEscapedCommandLine());
if (success) {
State.store.linuxAutoStartEnabled = true;
} }
return success;
}, },
disable() { disable: () => {
const success = requestBackground(false, []); if (isFlatpak) {
if (success) { } else {
State.store.linuxAutoStartEnabled = false; rmSync(file, { force: true });
} }
return success;
} }
}; };
} }
const autoStartWindowsMac: AutoStart = { const autoStartWindowsMac: AutoStart = {
isEnabled: () => app.getLoginItemSettings().openAtLogin, isEnabled: () => app.getLoginItemSettings().openAtLogin,
enable: () => wasAutoStarted: () => app.getLoginItemSettings().wasOpenedAtLogin,
app.setLoginItemSettings({ enable: () => app.setLoginItemSettings({ openAtLogin: true }),
openAtLogin: true,
args: Settings.store.autoStartMinimized ? ["--start-minimized"] : []
}),
disable: () => app.setLoginItemSettings({ openAtLogin: false }) disable: () => app.setLoginItemSettings({ openAtLogin: false })
}; };
// The portal call uses the app id by default, which is org.chromium.Chromium, even in packaged Vesktop. export const autoStart = process.platform === "linux" ? makeAutoStartLinux() : autoStartWindowsMac;
// This leads to an autostart entry named "Chromium" instead of "Vesktop".
// Thus, only use the portal inside Flatpak, where the app is actually correct.
// Maybe there is a way to fix it outside of flatpak, but I couldn't figure it out.
export const autoStart =
process.platform !== "linux"
? autoStartWindowsMac
: IS_FLATPAK
? makeAutoStartLinuxPortal()
: makeAutoStartLinuxDesktop();
Settings.addChangeListener("autoStartMinimized", () => {
if (!autoStart.isEnabled()) return;
autoStart.enable();
});

View File

@@ -1,145 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app } from "electron";
import { basename } from "path";
import { stripIndent } from "shared/utils/text";
import { parseArgs, ParseArgsOptionDescriptor } from "util";
type Option = ParseArgsOptionDescriptor & {
description: string;
hidden?: boolean;
options?: string[];
argumentName?: string;
};
const options = {
"start-minimized": {
default: false,
type: "boolean",
short: "m",
description: "Start the application minimized to the system tray"
},
version: {
type: "boolean",
short: "v",
description: "Print the application version and exit"
},
help: {
type: "boolean",
short: "h",
description: "Print help information and exit"
},
"user-agent": {
type: "string",
argumentName: "ua",
description: "Set a custom User-Agent. May trigger anti-spam or break voice chat"
},
"user-agent-os": {
type: "string",
description: "Set User-Agent to a specific operating system. May trigger anti-spam or break voice chat",
options: ["windows", "linux", "darwin"]
}
} satisfies Record<string, Option>;
// only for help display
const extraOptions = {
"enable-features": {
type: "string",
description: "Enable specific Chromium features",
argumentName: "feature1,feature2,…"
},
"disable-features": {
type: "string",
description: "Disable specific Chromium features",
argumentName: "feature1,feature2,…"
},
"ozone-platform": {
hidden: process.platform !== "linux",
type: "string",
description: "Whether to run Vesktop in Wayland or X11 (XWayland)",
options: ["x11", "wayland"]
}
} satisfies Record<string, Option>;
const args = basename(process.argv[0]).toLowerCase().startsWith("electron")
? process.argv.slice(2)
: process.argv.slice(1);
export const CommandLine = parseArgs({
args,
options,
strict: false as true, // we manually check later, so cast to true to get better types
allowPositionals: true
});
export function checkCommandLineForHelpOrVersion() {
const { help, version } = CommandLine.values;
if (version) {
console.log(`Vesktop v${app.getVersion()}`);
app.exit(0);
}
if (help) {
const base = stripIndent`
Vesktop v${app.getVersion()}
Usage: ${basename(process.execPath)} [options] [url]
Electron Options:
See <https://www.electronjs.org/docs/latest/api/command-line-switches#electron-cli-flags>
Chromium Options:
See <https://peter.sh/experiments/chromium-command-line-switches> - only some of them work
Vesktop Options:
`;
const optionLines = Object.entries(options)
.sort(([a], [b]) => a.localeCompare(b))
.concat(Object.entries(extraOptions))
.filter(([, opt]) => !("hidden" in opt && opt.hidden))
.map(([name, opt]) => {
const flags = [
"short" in opt && `-${opt.short}`,
`--${name}`,
opt.type !== "boolean" &&
("options" in opt ? `<${opt.options.join(" | ")}>` : `<${opt.argumentName ?? opt.type}>`)
]
.filter(Boolean)
.join(" ");
return [flags, opt.description];
});
const padding = optionLines.reduce((max, [flags]) => Math.max(max, flags.length), 0) + 4;
const optionsHelp = optionLines
.map(([flags, description]) => ` ${flags.padEnd(padding, " ")}${description}`)
.join("\n");
console.log(base + "\n" + optionsHelp);
app.exit(0);
}
for (const [name, def] of Object.entries(options)) {
const value = CommandLine.values[name];
if (value == null) continue;
if (typeof value !== def.type) {
console.error(`Invalid options. Expected ${def.type === "boolean" ? "no" : "an"} argument for --${name}`);
app.exit(1);
}
if ("options" in def && !def.options?.includes(value as string)) {
console.error(`Invalid value for --${name}: ${value}\nExpected one of: ${def.options.join(", ")}`);
app.exit(1);
}
}
}
checkCommandLineForHelpOrVersion();

View File

@@ -1,36 +1,23 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { app } from "electron"; import { app } from "electron";
import { existsSync, mkdirSync } from "fs"; import { join } from "path";
import { dirname, join } from "path";
import { CommandLine } from "./cli";
const vesktopDir = dirname(process.execPath);
export const PORTABLE =
process.platform === "win32" &&
!process.execPath.toLowerCase().endsWith("electron.exe") &&
!existsSync(join(vesktopDir, "Uninstall Vesktop.exe"));
export const DATA_DIR =
process.env.VENCORD_USER_DATA_DIR || (PORTABLE ? join(vesktopDir, "Data") : join(app.getPath("userData")));
mkdirSync(DATA_DIR, { recursive: true });
export const SESSION_DATA_DIR = join(DATA_DIR, "sessionData");
app.setPath("sessionData", SESSION_DATA_DIR);
export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR || join(app.getPath("userData"), "VencordDesktop");
export const VENCORD_SETTINGS_DIR = join(DATA_DIR, "settings"); export const VENCORD_SETTINGS_DIR = join(DATA_DIR, "settings");
mkdirSync(VENCORD_SETTINGS_DIR, { recursive: true });
export const VENCORD_QUICKCSS_FILE = join(VENCORD_SETTINGS_DIR, "quickCss.css"); export const VENCORD_QUICKCSS_FILE = join(VENCORD_SETTINGS_DIR, "quickCss.css");
export const VENCORD_SETTINGS_FILE = join(VENCORD_SETTINGS_DIR, "settings.json"); export const VENCORD_SETTINGS_FILE = join(VENCORD_SETTINGS_DIR, "settings.json");
export const VENCORD_THEMES_DIR = join(DATA_DIR, "themes"); export const VENCORD_THEMES_DIR = join(DATA_DIR, "themes");
// needs to be inline require because of circular dependency
// as otherwise "DATA_DIR" (which is used by ./settings) will be uninitialised
export const VENCORD_FILES_DIR =
(require("./settings") as typeof import("./settings")).Settings.store.vencordDir || join(DATA_DIR, "vencordDist");
export const USER_AGENT = `Vesktop/${app.getVersion()} (https://github.com/Vencord/Vesktop)`; export const USER_AGENT = `Vesktop/${app.getVersion()} (https://github.com/Vencord/Vesktop)`;
// dimensions shamelessly stolen from Discord Desktop :3 // dimensions shamelessly stolen from Discord Desktop :3
@@ -39,23 +26,16 @@ export const MIN_HEIGHT = 500;
export const DEFAULT_WIDTH = 1280; export const DEFAULT_WIDTH = 1280;
export const DEFAULT_HEIGHT = 720; export const DEFAULT_HEIGHT = 720;
export const DISCORD_HOSTNAMES = ["discord.com", "canary.discord.com", "ptb.discord.com"]; const UserAgents = {
darwin: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
const VersionString = `AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${process.versions.chrome.split(".")[0]}.0.0.0 Safari/537.36`; linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
const BrowserUserAgents = { windows:
darwin: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ${VersionString}`, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
linux: `Mozilla/5.0 (X11; Linux x86_64) ${VersionString}`,
windows: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) ${VersionString}`
}; };
export const BrowserUserAgent = export const UserAgent = UserAgents[process.platform] || UserAgents.windows;
CommandLine.values["user-agent"] ||
BrowserUserAgents[CommandLine.values["user-agent-os"] || process.platform] ||
BrowserUserAgents.windows;
export const enum MessageBoxChoice { export const enum MessageBoxChoice {
Default, Default,
Cancel Cancel
} }
export const IS_FLATPAK = process.env.FLATPAK_ID !== undefined;

View File

@@ -1,40 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app } from "electron";
import { join } from "path";
import { STATIC_DIR } from "shared/paths";
let libVesktop: typeof import("libvesktop") | null = null;
function loadLibVesktop() {
try {
if (!libVesktop) {
libVesktop = require(join(STATIC_DIR, `dist/libvesktop-${process.arch}.node`));
}
} catch (e) {
console.error("Failed to load libvesktop:", e);
}
return libVesktop;
}
export function getAccentColor() {
return loadLibVesktop()?.getAccentColor() ?? null;
}
export function updateUnityLauncherCount(count: number) {
const libVesktop = loadLibVesktop();
if (!libVesktop) {
return app.setBadgeCount(count);
}
return libVesktop.updateUnityLauncherCount(count);
}
export function requestBackground(autoStart: boolean, commandLine: string[]) {
return loadLibVesktop()?.requestBackground(autoStart, commandLine) ?? false;
}

View File

@@ -1,15 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EventEmitter } from "events";
import { UserAssetType } from "./userAssets";
export const AppEvents = new EventEmitter<{
appLoaded: [];
userAssetChanged: [UserAssetType];
setTrayVariant: ["tray" | "trayUnread"];
}>();

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { app } from "electron"; import { app } from "electron";
@@ -9,45 +9,45 @@ import { BrowserWindow } from "electron/main";
import { copyFileSync, mkdirSync, readdirSync } from "fs"; import { copyFileSync, mkdirSync, readdirSync } from "fs";
import { join } from "path"; import { join } from "path";
import { SplashProps } from "shared/browserWinProperties"; import { SplashProps } from "shared/browserWinProperties";
import { ICON_PATH, VIEW_DIR } from "shared/paths";
import { autoStart } from "./autoStart"; import { autoStart } from "./autoStart";
import { DATA_DIR } from "./constants"; import { DATA_DIR } from "./constants";
import { createWindows } from "./mainWindow"; import { createWindows } from "./mainWindow";
import { Settings, State } from "./settings"; import { Settings } from "./settings";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { loadView } from "./vesktopStatic";
interface Data { interface Data {
minimizeToTray: boolean;
discordBranch: "stable" | "canary" | "ptb"; discordBranch: "stable" | "canary" | "ptb";
minimizeToTray?: "on"; autoStart: boolean;
autoStart?: "on"; importSettings: boolean;
importSettings?: "on"; richPresence: boolean;
richPresence?: "on";
} }
export function createFirstLaunchTour() { export function createFirstLaunchTour() {
const win = new BrowserWindow({ const win = new BrowserWindow({
...SplashProps, ...SplashProps,
transparent: false,
frame: true, frame: true,
autoHideMenuBar: true, autoHideMenuBar: true,
height: 550, height: 470,
width: 600 width: 550,
icon: ICON_PATH
}); });
makeLinksOpenExternally(win); makeLinksOpenExternally(win);
loadView(win, "first-launch.html"); win.loadFile(join(VIEW_DIR, "first-launch.html"));
win.webContents.addListener("console-message", (_e, _l, msg) => { win.webContents.addListener("console-message", (_e, _l, msg) => {
if (msg === "cancel") return app.exit(); if (msg === "cancel") return app.exit();
if (!msg.startsWith("form:")) return; if (!msg.startsWith("form:")) return;
const data = JSON.parse(msg.slice(5)) as Data; const data = JSON.parse(msg.slice(5)) as Data;
State.store.firstLaunch = false; Settings.store.minimizeToTray = data.minimizeToTray;
Settings.store.discordBranch = data.discordBranch; Settings.store.discordBranch = data.discordBranch;
Settings.store.minimizeToTray = !!data.minimizeToTray; Settings.store.firstLaunch = false;
Settings.store.arRPC = !!data.richPresence; Settings.store.arRPC = data.richPresence;
if (data.autoStart) autoStart.enable(); if (data.autoStart) autoStart.enable();
@@ -62,11 +62,7 @@ export function createFirstLaunchTour() {
copyFileSync(join(from, file), join(to, file)); copyFileSync(join(from, file), join(to, file));
} }
} catch (e) { } catch (e) {
if (e instanceof Error && "code" in e && e.code === "ENOENT") { console.error("Failed to import settings:", e);
console.log("No Vencord settings found to import.");
} else {
console.error("Failed to import Vencord settings:", e);
}
} }
} }

View File

@@ -1,105 +1,46 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import "./cli";
import "./updater";
import "./ipc"; import "./ipc";
import "./userAssets";
import "./vesktopProtocol";
import { app, BrowserWindow, nativeTheme } from "electron"; import { app, BrowserWindow } from "electron";
import { checkUpdates } from "updater/main";
import { DATA_DIR } from "./constants"; import { DATA_DIR } from "./constants";
import { createFirstLaunchTour } from "./firstLaunch"; import { createFirstLaunchTour } from "./firstLaunch";
import { createWindows, mainWin } from "./mainWindow"; import { createWindows, mainWin } from "./mainWindow";
import { registerMediaPermissionsHandler } from "./mediaPermissions"; import { registerMediaPermissionsHandler } from "./mediaPermissions";
import { registerScreenShareHandler } from "./screenShare"; import { registerScreenShareHandler } from "./screenShare";
import { Settings, State } from "./settings"; import { Settings } from "./settings";
import { setAsDefaultProtocolClient } from "./utils/setAsDefaultProtocolClient";
import { isDeckGameMode } from "./utils/steamOS";
console.log("Vesktop v" + app.getVersion()); if (IS_DEV) {
require("source-map-support").install();
}
// Make the Vencord files use our DATA_DIR // Make the Vencord files use our DATA_DIR
process.env.VENCORD_USER_DATA_DIR = DATA_DIR; process.env.VENCORD_USER_DATA_DIR = DATA_DIR;
const isLinux = process.platform === "linux";
export let enableHardwareAcceleration = true;
function init() { function init() {
setAsDefaultProtocolClient("discord"); const { disableSmoothScroll } = Settings.store;
const { disableSmoothScroll, hardwareAcceleration, hardwareVideoAcceleration } = Settings.store;
const enabledFeatures = new Set(app.commandLine.getSwitchValue("enable-features").split(","));
const disabledFeatures = new Set(app.commandLine.getSwitchValue("disable-features").split(","));
app.commandLine.removeSwitch("enable-features");
app.commandLine.removeSwitch("disable-features");
if (hardwareAcceleration === false || process.argv.includes("--disable-gpu")) {
enableHardwareAcceleration = false;
app.disableHardwareAcceleration();
} else {
if (hardwareVideoAcceleration) {
enabledFeatures.add("AcceleratedVideoEncoder");
enabledFeatures.add("AcceleratedVideoDecoder");
if (isLinux) {
enabledFeatures.add("AcceleratedVideoDecodeLinuxGL");
enabledFeatures.add("AcceleratedVideoDecodeLinuxZeroCopyGL");
}
}
}
if (disableSmoothScroll) { if (disableSmoothScroll) {
app.commandLine.appendSwitch("disable-smooth-scrolling"); app.commandLine.appendSwitch("disable-smooth-scrolling");
} }
// disable renderer backgrounding to prevent the app from unloading when in the background
// https://github.com/electron/electron/issues/2822
// https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
app.commandLine.appendSwitch("disable-renderer-backgrounding");
app.commandLine.appendSwitch("disable-background-timer-throttling");
app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
if (process.platform === "win32") {
disabledFeatures.add("CalculateNativeWinOcclusion");
}
// work around chrome 66 disabling autoplay by default // work around chrome 66 disabling autoplay by default
app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required"); app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
// WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows. // WinRetrieveSuggestionsOnlyOnDemand: Work around electron 13 bug w/ async spellchecking on Windows.
// HardwareMediaKeyHandling, MediaSessionService: Prevent Discord from registering as a media service. // HardwareMediaKeyHandling,MediaSessionService: Prevent Discord from registering as a media service.
disabledFeatures.add("WinRetrieveSuggestionsOnlyOnDemand"); //
disabledFeatures.add("HardwareMediaKeyHandling"); // WidgetLayering (Vencord Added): Fix DevTools context menus https://github.com/electron/electron/issues/38790
disabledFeatures.add("MediaSessionService"); app.commandLine.appendSwitch(
"disable-features",
if (isLinux) { "WinRetrieveSuggestionsOnlyOnDemand,HardwareMediaKeyHandling,MediaSessionService,WidgetLayering"
// Support TTS on Linux using https://wiki.archlinux.org/title/Speech_dispatcher );
app.commandLine.appendSwitch("enable-speech-dispatcher");
}
disabledFeatures.forEach(feat => enabledFeatures.delete(feat));
const enabledFeaturesArray = enabledFeatures.values().filter(Boolean).toArray();
const disabledFeaturesArray = disabledFeatures.values().filter(Boolean).toArray();
if (enabledFeaturesArray.length) {
app.commandLine.appendSwitch("enable-features", enabledFeaturesArray.join(","));
console.log("Enabled Chromium features:", enabledFeaturesArray.join(", "));
}
if (disabledFeaturesArray.length) {
app.commandLine.appendSwitch("disable-features", disabledFeaturesArray.join(","));
console.log("Disabled Chromium features:", disabledFeaturesArray.join(", "));
}
// In the Flatpak on SteamOS the theme is detected as light, but SteamOS only has a dark mode, so we just override it
if (isDeckGameMode) nativeTheme.themeSource = "dark";
app.on("second-instance", (_event, _cmdLine, _cwd, data: any) => { app.on("second-instance", (_event, _cmdLine, _cwd, data: any) => {
if (data.IS_DEV) app.quit(); if (data.IS_DEV) app.quit();
@@ -111,7 +52,8 @@ function init() {
}); });
app.whenReady().then(async () => { app.whenReady().then(async () => {
if (process.platform === "win32") app.setAppUserModelId("dev.vencord.vesktop"); checkUpdates();
if (process.platform === "win32") app.setAppUserModelId("dev.vencord.desktop");
registerScreenShareHandler(); registerScreenShareHandler();
registerMediaPermissionsHandler(); registerMediaPermissionsHandler();
@@ -137,19 +79,13 @@ if (!app.requestSingleInstanceLock({ IS_DEV })) {
} }
async function bootstrap() { async function bootstrap() {
if (!Object.hasOwn(State.store, "firstLaunch")) { if (!Object.hasOwn(Settings.store, "firstLaunch")) {
createFirstLaunchTour(); createFirstLaunchTour();
} else { } else {
createWindows(); createWindows();
} }
} }
// MacOS only event
export let darwinURL: string | undefined;
app.on("open-url", (_, url) => {
darwinURL = url;
});
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit(); if (process.platform !== "darwin") app.quit();
}); });

View File

@@ -1,26 +1,15 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
if (process.platform === "linux") import("./venmic"); if (process.platform === "linux") import("./virtmic");
import { execFile } from "child_process"; import { execFile } from "child_process";
import { import { app, BrowserWindow, dialog, RelaunchOptions, session, shell } from "electron";
app,
BrowserWindow,
clipboard,
dialog,
IpcMainInvokeEvent,
nativeImage,
RelaunchOptions,
session,
shell
} from "electron";
import { mkdirSync, readFileSync, watch } from "fs"; import { mkdirSync, readFileSync, watch } from "fs";
import { open, readFile } from "fs/promises"; import { open, readFile } from "fs/promises";
import { enableHardwareAcceleration } from "main";
import { release } from "os"; import { release } from "os";
import { join } from "path"; import { join } from "path";
import { debounce } from "shared/utils/debounce"; import { debounce } from "shared/utils/debounce";
@@ -28,14 +17,11 @@ import { debounce } from "shared/utils/debounce";
import { IpcEvents } from "../shared/IpcEvents"; import { IpcEvents } from "../shared/IpcEvents";
import { setBadgeCount } from "./appBadge"; import { setBadgeCount } from "./appBadge";
import { autoStart } from "./autoStart"; import { autoStart } from "./autoStart";
import { VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants"; import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants";
import { mainWin } from "./mainWindow"; import { mainWin } from "./mainWindow";
import { Settings, State } from "./settings"; import { Settings } from "./settings";
import { handle, handleSync } from "./utils/ipcWrappers"; import { handle, handleSync } from "./utils/ipcWrappers";
import { PopoutWindows } from "./utils/popout";
import { isDeckGameMode, showGamePage } from "./utils/steamOS";
import { isValidVencordInstall } from "./utils/vencordLoader"; import { isValidVencordInstall } from "./utils/vencordLoader";
import { VENCORD_FILES_DIR } from "./vencordFilesDir";
handleSync(IpcEvents.GET_VENCORD_PRELOAD_FILE, () => join(VENCORD_FILES_DIR, "vencordDesktopPreload.js")); handleSync(IpcEvents.GET_VENCORD_PRELOAD_FILE, () => join(VENCORD_FILES_DIR, "vencordDesktopPreload.js"));
handleSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT, () => handleSync(IpcEvents.GET_VENCORD_RENDERER_SCRIPT, () =>
@@ -47,7 +33,6 @@ handleSync(IpcEvents.GET_RENDERER_CSS_FILE, () => join(__dirname, "renderer.css"
handleSync(IpcEvents.GET_SETTINGS, () => Settings.plain); handleSync(IpcEvents.GET_SETTINGS, () => Settings.plain);
handleSync(IpcEvents.GET_VERSION, () => app.getVersion()); handleSync(IpcEvents.GET_VERSION, () => app.getVersion());
handleSync(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION, () => enableHardwareAcceleration);
handleSync( handleSync(
IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY, IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY,
@@ -62,14 +47,11 @@ handle(IpcEvents.SET_SETTINGS, (_, settings: typeof Settings.store, path?: strin
Settings.setData(settings, path); Settings.setData(settings, path);
}); });
handle(IpcEvents.RELAUNCH, async () => { handle(IpcEvents.RELAUNCH, () => {
const options: RelaunchOptions = { const options: RelaunchOptions = {
args: process.argv.slice(1).concat(["--relaunch"]) args: process.argv.slice(1).concat(["--relaunch"])
}; };
if (isDeckGameMode) { if (app.isPackaged && process.env.APPIMAGE) {
// We can't properly relaunch when running under gamescope, but we can at least navigate to our page in Steam.
await showGamePage();
} else if (app.isPackaged && process.env.APPIMAGE) {
execFile(process.env.APPIMAGE, options.args); execFile(process.env.APPIMAGE, options.args);
} else { } else {
app.relaunch(options); app.relaunch(options);
@@ -81,34 +63,35 @@ handle(IpcEvents.SHOW_ITEM_IN_FOLDER, (_, path) => {
shell.showItemInFolder(path); shell.showItemInFolder(path);
}); });
function getWindow(e: IpcMainInvokeEvent, key?: string) {
return key ? PopoutWindows.get(key)! : (BrowserWindow.fromWebContents(e.sender) ?? mainWin);
}
handle(IpcEvents.FOCUS, () => { handle(IpcEvents.FOCUS, () => {
if (process.platform === "win32") mainWin.minimize(); // Windows is weird
mainWin.restore();
mainWin.show(); mainWin.show();
mainWin.setSkipTaskbar(false);
}); });
handle(IpcEvents.CLOSE, (e, key?: string) => { handle(IpcEvents.CLOSE, e => {
getWindow(e, key).close(); (BrowserWindow.fromWebContents(e.sender) ?? e.sender).close();
}); });
handle(IpcEvents.MINIMIZE, (e, key?: string) => { handle(IpcEvents.MINIMIZE, e => {
getWindow(e, key).minimize(); mainWin.minimize();
}); });
handle(IpcEvents.MAXIMIZE, (e, key?: string) => { handle(IpcEvents.MAXIMIZE, e => {
const win = getWindow(e, key); if (mainWin.isMaximized()) {
if (win.isMaximized()) { mainWin.unmaximize();
win.unmaximize();
} else { } else {
win.maximize(); mainWin.maximize();
} }
}); });
handleSync(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES, e => { handle(IpcEvents.SPELLCHECK_SET_LANGUAGES, (_, languages: string[]) => {
e.returnValue = session.defaultSession.availableSpellCheckerLanguages; const ses = session.defaultSession;
const available = ses.availableSpellCheckerLanguages;
const applicable = languages.filter(l => available.includes(l)).slice(0, 3);
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
}); });
handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => { handle(IpcEvents.SPELLCHECK_REPLACE_MISSPELLING, (e, word: string) => {
@@ -119,14 +102,7 @@ handle(IpcEvents.SPELLCHECK_ADD_TO_DICTIONARY, (e, word: string) => {
e.sender.session.addWordToSpellCheckerDictionary(word); e.sender.session.addWordToSpellCheckerDictionary(word);
}); });
handleSync(IpcEvents.GET_VENCORD_DIR, e => (e.returnValue = State.store.vencordDir)); handle(IpcEvents.SELECT_VENCORD_DIR, async () => {
handle(IpcEvents.SELECT_VENCORD_DIR, async (_e, value?: null) => {
if (value === null) {
delete State.store.vencordDir;
return "ok";
}
const res = await dialog.showOpenDialog(mainWin!, { const res = await dialog.showOpenDialog(mainWin!, {
properties: ["openDirectory"] properties: ["openDirectory"]
}); });
@@ -135,36 +111,11 @@ handle(IpcEvents.SELECT_VENCORD_DIR, async (_e, value?: null) => {
const dir = res.filePaths[0]; const dir = res.filePaths[0];
if (!isValidVencordInstall(dir)) return "invalid"; if (!isValidVencordInstall(dir)) return "invalid";
State.store.vencordDir = dir; return dir;
return "ok";
}); });
handle(IpcEvents.SET_BADGE_COUNT, (_, count: number) => setBadgeCount(count)); handle(IpcEvents.SET_BADGE_COUNT, (_, count: number) => setBadgeCount(count));
handle(IpcEvents.FLASH_FRAME, (_, flag: boolean) => {
if (!mainWin || mainWin.isDestroyed() || (flag && mainWin.isFocused())) return;
mainWin.flashFrame(flag);
});
handle(IpcEvents.CLIPBOARD_COPY_IMAGE, async (_, buf: ArrayBuffer, src: string) => {
clipboard.write({
html: `<img src="${src.replaceAll('"', '\\"')}">`,
image: nativeImage.createFromBuffer(Buffer.from(buf))
});
});
function openDebugPage(page: string) {
const win = new BrowserWindow({
autoHideMenuBar: true
});
win.loadURL(page);
}
handle(IpcEvents.DEBUG_LAUNCH_GPU, () => openDebugPage("chrome://gpu"));
handle(IpcEvents.DEBUG_LAUNCH_WEBRTC_INTERNALS, () => openDebugPage("chrome://webrtc-internals"));
function readCss() { function readCss() {
return readFile(VENCORD_QUICKCSS_FILE, "utf-8").catch(() => ""); return readFile(VENCORD_QUICKCSS_FILE, "utf-8").catch(() => "");
} }

View File

@@ -1,61 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { randomUUID } from "crypto";
import { ipcMain } from "electron";
import { IpcEvents } from "shared/IpcEvents";
import { mainWin } from "./mainWindow";
const resolvers = new Map<string, Record<"resolve" | "reject", (data: any) => void>>();
export interface IpcMessage {
nonce: string;
message: string;
data?: any;
}
export interface IpcResponse {
nonce: string;
ok: boolean;
data?: any;
}
/**
* Sends a message to the renderer process and waits for a response.
* `data` must be serializable as it will be sent over IPC.
*
* You must add a handler for the message in the renderer process.
*/
export function sendRendererCommand<T = any>(message: string, data?: any) {
if (mainWin.isDestroyed()) {
console.warn("Main window is destroyed, cannot send IPC command:", message);
return Promise.reject(new Error("Main window is destroyed"));
}
const nonce = randomUUID();
const promise = new Promise<T>((resolve, reject) => {
resolvers.set(nonce, { resolve, reject });
});
mainWin.webContents.send(IpcEvents.IPC_COMMAND, { nonce, message, data });
return promise;
}
ipcMain.on(IpcEvents.IPC_COMMAND, (_event, { nonce, ok, data }: IpcResponse) => {
const resolver = resolvers.get(nonce);
if (!resolver) throw new Error(`Unknown message: ${nonce}`);
if (ok) {
resolver.resolve(data);
} else {
resolver.reject(data);
}
resolvers.delete(nonce);
});

View File

@@ -1,43 +1,48 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { import {
app, app,
BrowserWindow, BrowserWindow,
BrowserWindowConstructorOptions, BrowserWindowConstructorOptions,
dialog,
Menu, Menu,
MenuItemConstructorOptions, MenuItemConstructorOptions,
nativeTheme, nativeTheme,
Rectangle, Tray
screen,
session
} from "electron"; } from "electron";
import { rm } from "fs/promises";
import { join } from "path"; import { join } from "path";
import { IpcCommands, IpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
import { isTruthy } from "shared/utils/guards"; import { isTruthy } from "shared/utils/guards";
import { once } from "shared/utils/once"; import { once } from "shared/utils/once";
import type { SettingsStore } from "shared/utils/SettingsStore"; import type { SettingsStore } from "shared/utils/SettingsStore";
import { ICON_PATH } from "../shared/paths";
import { createAboutWindow } from "./about"; import { createAboutWindow } from "./about";
import { initArRPC } from "./arrpc"; import { initArRPC } from "./arrpc";
import { CommandLine } from "./cli"; import { autoStart } from "./autoStart";
import { BrowserUserAgent, DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH } from "./constants"; import {
import { AppEvents } from "./events"; DATA_DIR,
import { darwinURL } from "./index"; DEFAULT_HEIGHT,
import { sendRendererCommand } from "./ipcCommands"; DEFAULT_WIDTH,
import { Settings, State, VencordSettings } from "./settings"; MessageBoxChoice,
import { createSplashWindow, updateSplashMessage } from "./splash"; MIN_HEIGHT,
import { destroyTray, initTray } from "./tray"; MIN_WIDTH,
import { clearData } from "./utils/clearData"; UserAgent,
VENCORD_FILES_DIR
} from "./constants";
import { Settings, VencordSettings } from "./settings";
import { createSplashWindow } from "./splash";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS"; import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS";
import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader"; import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
import { VENCORD_FILES_DIR } from "./vencordFilesDir";
let isQuitting = false; let isQuitting = false;
let tray: Tray;
applyDeckKeyboardFix(); applyDeckKeyboardFix();
@@ -68,6 +73,89 @@ function makeSettingsListenerHelpers<O extends object>(o: SettingsStore<O>) {
const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpers(Settings); const [addSettingsListener, removeSettingsListeners] = makeSettingsListenerHelpers(Settings);
const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings); const [addVencordSettingsListener, removeVencordSettingsListeners] = makeSettingsListenerHelpers(VencordSettings);
function initTray(win: BrowserWindow) {
const trayMenu = Menu.buildFromTemplate([
{
label: "Open",
click() {
win.show();
},
enabled: false
},
{
label: "About",
click: createAboutWindow
},
{
label: "Update Vencord",
async click() {
await downloadVencordFiles();
app.relaunch();
app.quit();
}
},
{
label: "Reset Vesktop",
async click() {
await clearData(win);
}
},
{
type: "separator"
},
{
label: "Relaunch",
click() {
app.relaunch();
app.quit();
}
},
{
label: "Quit Vesktop",
click() {
isQuitting = true;
app.quit();
}
}
]);
tray = new Tray(ICON_PATH);
tray.setToolTip("Vesktop");
tray.setContextMenu(trayMenu);
tray.on("click", () => win.show());
win.on("show", () => {
trayMenu.items[0].enabled = false;
});
win.on("hide", () => {
trayMenu.items[0].enabled = true;
});
}
async function clearData(win: BrowserWindow) {
const { response } = await dialog.showMessageBox(win, {
message: "Are you sure you want to reset Vesktop?",
detail: "This will log you out, clear caches and reset all your settings!\n\nVesktop will automatically restart after this operation.",
buttons: ["Yes", "No"],
cancelId: MessageBoxChoice.Cancel,
defaultId: MessageBoxChoice.Default,
type: "warning"
});
if (response === MessageBoxChoice.Cancel) return;
win.close();
await win.webContents.session.clearStorageData();
await win.webContents.session.clearCache();
await win.webContents.session.clearCodeCaches({});
await rm(DATA_DIR, { force: true, recursive: true });
app.relaunch();
app.quit();
}
type MenuItemList = Array<MenuItemConstructorOptions | false>; type MenuItemList = Array<MenuItemConstructorOptions | false>;
function initMenuBar(win: BrowserWindow) { function initMenuBar(win: BrowserWindow) {
@@ -114,13 +202,16 @@ function initMenuBar(win: BrowserWindow) {
label: "Settings", label: "Settings",
accelerator: "CmdOrCtrl+,", accelerator: "CmdOrCtrl+,",
async click() { async click() {
sendRendererCommand(IpcCommands.NAVIGATE_SETTINGS); mainWin.webContents.executeJavaScript(
"Vencord.Webpack.Common.SettingsRouter.open('My Account')"
);
} }
}, },
{ {
type: "separator" type: "separator"
}, },
{ {
label: "Hide Vesktop", // Should probably remove the label, but it says "Hide VencordDesktop" instead of "Hide Vesktop"
role: "hide" role: "hide"
}, },
{ {
@@ -159,7 +250,7 @@ function initMenuBar(win: BrowserWindow) {
} }
] satisfies MenuItemList; ] satisfies MenuItemList;
const menuItems = [ const menu = Menu.buildFromTemplate([
{ {
label: "Vesktop", label: "Vesktop",
role: "appMenu", role: "appMenu",
@@ -168,18 +259,63 @@ function initMenuBar(win: BrowserWindow) {
{ role: "fileMenu" }, { role: "fileMenu" },
{ role: "editMenu" }, { role: "editMenu" },
{ role: "viewMenu" }, { role: "viewMenu" },
isDarwin && { role: "windowMenu" } { role: "windowMenu" }
] satisfies MenuItemList; ]);
const menu = Menu.buildFromTemplate(menuItems.filter(isTruthy));
Menu.setApplicationMenu(menu); Menu.setApplicationMenu(menu);
} }
function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
// We want the default window behaivour to apply in game mode since it expects everything to be fullscreen and maximized.
if (isDeckGameMode) return {};
const { x, y, width, height } = Settings.store.windowBounds ?? {};
const options = {
width: width ?? DEFAULT_WIDTH,
height: height ?? DEFAULT_HEIGHT
} as BrowserWindowConstructorOptions;
if (x != null && y != null) {
options.x = x;
options.y = y;
}
if (!Settings.store.disableMinSize) {
options.minWidth = MIN_WIDTH;
options.minHeight = MIN_HEIGHT;
}
return options;
}
function getDarwinOptions(): BrowserWindowConstructorOptions {
const options = {
titleBarStyle: "hidden",
trafficLightPosition: { x: 10, y: 10 }
} as BrowserWindowConstructorOptions;
const { splashTheming, splashBackground } = Settings.store;
const { macosTranslucency } = VencordSettings.store;
if (macosTranslucency) {
options.vibrancy = "sidebar";
options.backgroundColor = "#ffffff00";
} else {
if (splashTheming) {
options.backgroundColor = splashBackground;
} else {
options.backgroundColor = nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff";
}
}
return options;
}
function initWindowBoundsListeners(win: BrowserWindow) { function initWindowBoundsListeners(win: BrowserWindow) {
const saveState = () => { const saveState = () => {
State.store.maximized = win.isMaximized(); Settings.store.maximized = win.isMaximized();
State.store.minimized = win.isMinimized(); Settings.store.minimized = win.isMinimized();
}; };
win.on("maximize", saveState); win.on("maximize", saveState);
@@ -187,7 +323,7 @@ function initWindowBoundsListeners(win: BrowserWindow) {
win.on("unmaximize", saveState); win.on("unmaximize", saveState);
const saveBounds = () => { const saveBounds = () => {
State.store.windowBounds = win.getBounds(); Settings.store.windowBounds = win.getBounds();
}; };
win.on("resize", saveBounds); win.on("resize", saveBounds);
@@ -196,10 +332,9 @@ function initWindowBoundsListeners(win: BrowserWindow) {
function initSettingsListeners(win: BrowserWindow) { function initSettingsListeners(win: BrowserWindow) {
addSettingsListener("tray", enable => { addSettingsListener("tray", enable => {
if (enable) initTray(win, q => (isQuitting = q)); if (enable) initTray(win);
else destroyTray(); else tray?.destroy();
}); });
addSettingsListener("disableMinSize", disable => { addSettingsListener("disableMinSize", disable => {
if (disable) { if (disable) {
// 0 no work // 0 no work
@@ -228,143 +363,12 @@ function initSettingsListeners(win: BrowserWindow) {
addSettingsListener("enableMenu", enabled => { addSettingsListener("enableMenu", enabled => {
win.setAutoHideMenuBar(enabled ?? false); win.setAutoHideMenuBar(enabled ?? false);
}); });
addSettingsListener("spellCheckLanguages", languages => initSpellCheckLanguages(win, languages));
}
async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) {
languages ??= await sendRendererCommand(IpcCommands.GET_LANGUAGES);
if (!languages) return;
const ses = session.defaultSession;
const available = ses.availableSpellCheckerLanguages;
const applicable = languages.filter(l => available.includes(l)).slice(0, 5);
if (applicable.length) ses.setSpellCheckerLanguages(applicable);
} }
function initSpellCheck(win: BrowserWindow) { function initSpellCheck(win: BrowserWindow) {
win.webContents.on("context-menu", (_, data) => { win.webContents.on("context-menu", (_, data) => {
win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions); win.webContents.send(IpcEvents.SPELLCHECK_RESULT, data.misspelledWord, data.dictionarySuggestions);
}); });
initSpellCheckLanguages(win, Settings.store.spellCheckLanguages);
}
function initDevtoolsListeners(win: BrowserWindow) {
win.webContents.on("devtools-opened", () => {
win.webContents.send(IpcEvents.DEVTOOLS_OPENED);
});
win.webContents.on("devtools-closed", () => {
win.webContents.send(IpcEvents.DEVTOOLS_CLOSED);
});
}
function initStaticTitle(win: BrowserWindow) {
const listener = (e: { preventDefault: Function }) => e.preventDefault();
if (Settings.store.staticTitle) win.on("page-title-updated", listener);
addSettingsListener("staticTitle", enabled => {
if (enabled) {
win.setTitle("Vesktop");
win.on("page-title-updated", listener);
} else {
win.off("page-title-updated", listener);
}
});
}
function getWindowBoundsOptions(): BrowserWindowConstructorOptions {
// We want the default window behaviour to apply in game mode since it expects everything to be fullscreen and maximized.
if (isDeckGameMode) return {};
const { x, y, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT } = State.store.windowBounds ?? {};
const options = { width, height } as BrowserWindowConstructorOptions;
if (x != null && y != null) {
function isInBounds(rect: Rectangle, display: Rectangle) {
return !(
rect.x + rect.width < display.x ||
rect.x > display.x + display.width ||
rect.y + rect.height < display.y ||
rect.y > display.y + display.height
);
}
const inBounds = screen.getAllDisplays().some(d => isInBounds({ x, y, width, height }, d.bounds));
if (inBounds) {
options.x = x;
options.y = y;
}
}
if (!Settings.store.disableMinSize) {
options.minWidth = MIN_WIDTH;
options.minHeight = MIN_HEIGHT;
}
return options;
}
function buildBrowserWindowOptions(): BrowserWindowConstructorOptions {
const { staticTitle, transparencyOption, enableMenu, customTitleBar, splashTheming, splashBackground } =
Settings.store;
const { frameless, transparent, macosVibrancyStyle } = VencordSettings.store;
const noFrame = frameless === true || customTitleBar === true;
const backgroundColor =
splashTheming !== false ? splashBackground : nativeTheme.shouldUseDarkColors ? "#313338" : "#ffffff";
const options: BrowserWindowConstructorOptions = {
show: Settings.store.enableSplashScreen === false && !CommandLine.values["start-minimized"],
backgroundColor,
webPreferences: {
nodeIntegration: false,
sandbox: false, // TODO
contextIsolation: true,
devTools: true,
preload: join(__dirname, "preload.js"),
spellcheck: true,
// disable renderer backgrounding to prevent the app from unloading when in the background
backgroundThrottling: false
},
frame: !noFrame,
autoHideMenuBar: enableMenu,
...getWindowBoundsOptions()
};
if (transparent) {
options.transparent = true;
options.backgroundColor = "#00000000";
}
if (transparencyOption && transparencyOption !== "none") {
options.backgroundColor = "#00000000";
options.backgroundMaterial = transparencyOption;
if (customTitleBar) {
options.transparent = true;
}
}
if (staticTitle) {
options.title = "Vesktop";
}
if (process.platform === "darwin") {
options.titleBarStyle = "hidden";
options.trafficLightPosition = { x: 10, y: 10 };
if (macosVibrancyStyle) {
options.vibrancy = macosVibrancyStyle;
options.backgroundColor = "#00000000";
}
}
return options;
} }
function createMainWindow() { function createMainWindow() {
@@ -372,10 +376,36 @@ function createMainWindow() {
removeSettingsListeners(); removeSettingsListeners();
removeVencordSettingsListeners(); removeVencordSettingsListeners();
const win = (mainWin = new BrowserWindow(buildBrowserWindowOptions())); const { staticTitle, transparencyOption, enableMenu, discordWindowsTitleBar } = Settings.store;
const { frameless } = VencordSettings.store;
const noFrame = frameless === true || (process.platform === "win32" && discordWindowsTitleBar === true);
const win = (mainWin = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: false,
sandbox: false,
contextIsolation: true,
devTools: true,
preload: join(__dirname, "preload.js"),
spellcheck: true
},
icon: ICON_PATH,
frame: !noFrame,
...(transparencyOption &&
transparencyOption !== "none" && {
backgroundColor: "#00000000",
backgroundMaterial: transparencyOption,
transparent: true
}),
...(staticTitle && { title: "Vesktop" }),
...(process.platform === "darwin" && getDarwinOptions()),
...getWindowBoundsOptions(),
autoHideMenuBar: enableMenu
}));
win.setMenuBarVisibility(false); win.setMenuBarVisibility(false);
if (process.platform === "darwin" && Settings.store.customTitleBar) win.setWindowButtonVisibility(false);
win.on("close", e => { win.on("close", e => {
const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false; const useTray = !isDeckGameMode && Settings.store.minimizeToTray !== false && Settings.store.tray !== false;
@@ -389,74 +419,43 @@ function createMainWindow() {
return false; return false;
}); });
win.on("focus", () => { if (Settings.store.staticTitle) win.on("page-title-updated", e => e.preventDefault());
win.flashFrame(false);
});
initWindowBoundsListeners(win); initWindowBoundsListeners(win);
if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") if (!isDeckGameMode && (Settings.store.tray ?? true) && process.platform !== "darwin") initTray(win);
initTray(win, q => (isQuitting = q));
initMenuBar(win); initMenuBar(win);
makeLinksOpenExternally(win); makeLinksOpenExternally(win);
initSettingsListeners(win); initSettingsListeners(win);
initSpellCheck(win); initSpellCheck(win);
initDevtoolsListeners(win);
initStaticTitle(win);
win.webContents.setUserAgent(BrowserUserAgent); win.webContents.setUserAgent(UserAgent);
// if the open-url event is fired (in index.ts) while starting up, darwinURL will be set. If not fall back to checking the process args (which Windows and Linux use for URI calling.) const subdomain =
// win.webContents.session.clearCache().then(() => { Settings.store.discordBranch === "canary" || Settings.store.discordBranch === "ptb"
loadUrl(darwinURL || process.argv.find(arg => arg.startsWith("discord://"))); ? `${Settings.store.discordBranch}.`
// }); : "";
win.loadURL(`https://${subdomain}discord.com/app`);
return win; return win;
} }
const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js"))); const runVencordMain = once(() => require(join(VENCORD_FILES_DIR, "vencordDesktopMain.js")));
export function loadUrl(uri: string | undefined) {
const branch = Settings.store.discordBranch;
const subdomain = branch === "canary" || branch === "ptb" ? `${branch}.` : "";
// we do not rely on 'did-finish-load' because it fires even if loadURL fails which triggers early detruction of the splash
mainWin
.loadURL(`https://${subdomain}discord.com/${uri ? new URL(uri).pathname.slice(1) || "app" : "app"}`)
.then(() => AppEvents.emit("appLoaded"))
.catch(error => retryUrl(error.url, error.code));
}
const retryDelay = 1000;
function retryUrl(url: string, description: string) {
console.log(`retrying in ${retryDelay}ms`);
updateSplashMessage(`Failed to load Discord: ${description}`);
setTimeout(() => loadUrl(url), retryDelay);
}
export async function createWindows() { export async function createWindows() {
const startMinimized = CommandLine.values["start-minimized"]; const shouldStartMinimized = Settings.store.startMinimized && autoStart.wasAutoStarted();
const splash = createSplashWindow(shouldStartMinimized);
let splash: BrowserWindow | undefined; // SteamOS letterboxes and scales it terribly, so just full screen it
if (Settings.store.enableSplashScreen !== false) { if (isDeckGameMode) splash.setFullScreen(true);
splash = createSplashWindow(startMinimized);
// SteamOS letterboxes and scales it terribly, so just full screen it
if (isDeckGameMode) splash.setFullScreen(true);
}
await ensureVencordFiles(); await ensureVencordFiles();
runVencordMain(); runVencordMain();
mainWin = createMainWindow(); mainWin = createMainWindow();
AppEvents.on("appLoaded", () => { mainWin.webContents.on("did-finish-load", () => {
splash?.destroy(); splash.destroy();
if (!startMinimized) { if (!shouldStartMinimized || isDeckGameMode) mainWin!.show();
if (splash) mainWin!.show();
if (State.store.maximized && !isDeckGameMode) mainWin!.maximize();
}
if (isDeckGameMode) { if (isDeckGameMode) {
// always use entire display // always use entire display
@@ -464,21 +463,11 @@ export async function createWindows() {
askToApplySteamLayout(mainWin); askToApplySteamLayout(mainWin);
} }
mainWin.once("show", () => {
if (State.store.maximized && !mainWin!.isMaximized() && !isDeckGameMode) {
mainWin!.maximize();
}
});
}); });
mainWin.webContents.on("did-navigate", (_, url: string, responseCode: number) => { mainWin.once("show", () => {
updateSplashMessage(""); // clear the splash message if (Settings.store.maximized && !mainWin!.isMaximized() && !isDeckGameMode) {
mainWin!.maximize();
// check url to ensure app doesn't loop
if (responseCode >= 300 && new URL(url).pathname !== `/app`) {
loadUrl(undefined);
console.warn(`'did-navigate': Caught bad page response: ${responseCode}, redirecting to main app`);
} }
}); });

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { session, systemPreferences } from "electron"; import { session, systemPreferences } from "electron";
@@ -12,13 +12,11 @@ export function registerMediaPermissionsHandler() {
session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => { session.defaultSession.setPermissionRequestHandler(async (_webContents, permission, callback, details) => {
let granted = true; let granted = true;
if ("mediaTypes" in details) { if (details.mediaTypes?.includes("audio")) {
if (details.mediaTypes?.includes("audio")) { granted = await systemPreferences.askForMediaAccess("microphone");
granted &&= await systemPreferences.askForMediaAccess("microphone"); }
} if (details.mediaTypes?.includes("video")) {
if (details.mediaTypes?.includes("video")) { granted &&= await systemPreferences.askForMediaAccess("camera");
granted &&= await systemPreferences.askForMediaAccess("camera");
}
} }
callback(granted); callback(granted);

View File

@@ -1,14 +1,13 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { desktopCapturer, session, Streams } from "electron"; import { desktopCapturer, session, Streams } from "electron";
import type { StreamPick } from "renderer/components/ScreenSharePicker"; import type { StreamPick } from "renderer/components/ScreenSharePicker";
import { IpcCommands, IpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
import { sendRendererCommand } from "./ipcCommands";
import { handle } from "./utils/ipcWrappers"; import { handle } from "./utils/ipcWrappers";
const isWayland = const isWayland =
@@ -50,11 +49,11 @@ export function registerScreenShareHandler() {
if (isWayland) { if (isWayland) {
const video = data[0]; const video = data[0];
if (video) { if (video) {
const stream = await sendRendererCommand<StreamPick>(IpcCommands.SCREEN_SHARE_PICKER, { const stream = await request.frame
screens: [video], .executeJavaScript(
skipPicker: true `Vesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify([video])},true)`
}).catch(() => null); )
.catch(() => null);
if (stream === null) return callback({}); if (stream === null) return callback({});
} }
@@ -62,13 +61,13 @@ export function registerScreenShareHandler() {
return; return;
} }
const choice = await sendRendererCommand<StreamPick>(IpcCommands.SCREEN_SHARE_PICKER, { const choice = await request.frame
screens: data, .executeJavaScript(`Vesktop.Components.ScreenShare.openScreenSharePicker(${JSON.stringify(data)})`)
skipPicker: false .then(e => e as StreamPick)
}).catch(e => { .catch(e => {
console.error("Error during screenshare picker", e); console.error("Error during screenshare picker", e);
return null; return null;
}); });
if (!choice) return callback({}); if (!choice) return callback({});

View File

@@ -1,19 +1,17 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { type Settings as TVencordSettings } from "@vencord/types/Vencord";
import { mkdirSync, readFileSync, writeFileSync } from "fs"; import { mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path"; import { dirname, join } from "path";
import type { Settings as TSettings, State as TState } from "shared/settings"; import type { Settings as TSettings } from "shared/settings";
import { SettingsStore } from "shared/utils/SettingsStore"; import { SettingsStore } from "shared/utils/SettingsStore";
import { DATA_DIR, VENCORD_SETTINGS_FILE } from "./constants"; import { DATA_DIR, VENCORD_SETTINGS_FILE } from "./constants";
const SETTINGS_FILE = join(DATA_DIR, "settings.json"); const SETTINGS_FILE = join(DATA_DIR, "settings.json");
const STATE_FILE = join(DATA_DIR, "state.json");
function loadSettings<T extends object = any>(file: string, name: string) { function loadSettings<T extends object = any>(file: string, name: string) {
let settings = {} as T; let settings = {} as T;
@@ -22,23 +20,18 @@ function loadSettings<T extends object = any>(file: string, name: string) {
try { try {
settings = JSON.parse(content); settings = JSON.parse(content);
} catch (err) { } catch (err) {
console.error(`Failed to parse ${name}.json:`, err); console.error(`Failed to parse ${name} settings.json:`, err);
} }
} catch {} } catch {}
const store = new SettingsStore(settings); const store = new SettingsStore(settings);
store.addGlobalChangeListener(o => { store.addGlobalChangeListener(o => {
try { mkdirSync(dirname(file), { recursive: true });
mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, JSON.stringify(o, null, 4));
writeFileSync(file, JSON.stringify(o, null, 4));
} catch (err) {
console.error(`Failed to save settings to ${name}.json:`, err);
}
}); });
return store; return store;
} }
export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop settings"); export const Settings = loadSettings<TSettings>(SETTINGS_FILE, "Vesktop");
export const VencordSettings = loadSettings<TVencordSettings>(VENCORD_SETTINGS_FILE, "Vencord settings"); export const VencordSettings = loadSettings<any>(VENCORD_SETTINGS_FILE, "Vencord");
export const State = loadSettings<TState>(STATE_FILE, "Vesktop state");

View File

@@ -1,32 +1,28 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { BrowserWindow } from "electron"; import { BrowserWindow } from "electron";
import { join } from "path"; import { join } from "path";
import { SplashProps } from "shared/browserWinProperties"; import { SplashProps } from "shared/browserWinProperties";
import { ICON_PATH, VIEW_DIR } from "shared/paths";
import { Settings } from "./settings"; import { Settings } from "./settings";
import { loadView } from "./vesktopStatic";
let splash: BrowserWindow | undefined;
export function createSplashWindow(startMinimized = false) { export function createSplashWindow(startMinimized = false) {
splash = new BrowserWindow({ const splash = new BrowserWindow({
...SplashProps, ...SplashProps,
show: !startMinimized, icon: ICON_PATH,
webPreferences: { show: !startMinimized
preload: join(__dirname, "splashPreload.js")
}
}); });
loadView(splash, "splash.html"); splash.loadFile(join(VIEW_DIR, "splash.html"));
const { splashBackground, splashColor, splashTheming, splashPixelated } = Settings.store; const { splashBackground, splashColor, splashTheming } = Settings.store;
if (splashTheming !== false) { if (splashTheming) {
if (splashColor) { if (splashColor) {
const semiTransparentSplashColor = splashColor.replace("rgb(", "rgba(").replace(")", ", 0.2)"); const semiTransparentSplashColor = splashColor.replace("rgb(", "rgba(").replace(")", ", 0.2)");
@@ -39,13 +35,5 @@ export function createSplashWindow(startMinimized = false) {
} }
} }
if (splashPixelated) {
splash.webContents.insertCSS(`img { image-rendering: pixelated; }`);
}
return splash; return splash;
} }
export function updateSplashMessage(message: string) {
if (splash && !splash.isDestroyed()) splash.webContents.send("update-splash-message", message);
}

View File

@@ -1,92 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, BrowserWindow, Menu, Tray } from "electron";
import { createAboutWindow } from "./about";
import { AppEvents } from "./events";
import { Settings } from "./settings";
import { resolveAssetPath } from "./userAssets";
import { clearData } from "./utils/clearData";
import { downloadVencordFiles } from "./utils/vencordLoader";
let tray: Tray;
let trayVariant: "tray" | "trayUnread" = "tray";
AppEvents.on("userAssetChanged", async asset => {
if (tray && (asset === "tray" || asset === "trayUnread")) {
tray.setImage(await resolveAssetPath(trayVariant));
}
});
AppEvents.on("setTrayVariant", async variant => {
if (trayVariant === variant) return;
trayVariant = variant;
if (!tray) return;
tray.setImage(await resolveAssetPath(trayVariant));
});
export function destroyTray() {
tray?.destroy();
}
export async function initTray(win: BrowserWindow, setIsQuitting: (val: boolean) => void) {
const onTrayClick = () => {
if (Settings.store.clickTrayToShowHide && win.isVisible()) win.hide();
else win.show();
};
const trayMenu = Menu.buildFromTemplate([
{
label: "Open",
click() {
win.show();
}
},
{
label: "About",
click: createAboutWindow
},
{
label: "Repair Vencord",
async click() {
await downloadVencordFiles();
app.relaunch();
app.quit();
}
},
{
label: "Reset Vesktop",
async click() {
await clearData(win);
}
},
{
type: "separator"
},
{
label: "Restart",
click() {
app.relaunch();
app.quit();
}
},
{
label: "Quit",
click() {
setIsQuitting(true);
app.quit();
}
}
]);
tray = new Tray(await resolveAssetPath(trayVariant));
tray.setToolTip("Vesktop");
tray.setContextMenu(trayMenu);
tray.on("click", onTrayClick);
}

View File

@@ -1,81 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, BrowserWindow, ipcMain } from "electron";
import { autoUpdater, UpdateInfo } from "electron-updater";
import { join } from "path";
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
import { Millis } from "shared/utils/millis";
import { State } from "./settings";
import { handle } from "./utils/ipcWrappers";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { loadView } from "./vesktopStatic";
let updaterWindow: BrowserWindow | null = null;
autoUpdater.on("update-available", update => {
if (State.store.updater?.ignoredVersion === update.version) return;
if ((State.store.updater?.snoozeUntil ?? 0) > Date.now()) return;
openUpdater(update);
});
autoUpdater.on("update-downloaded", () => setTimeout(() => autoUpdater.quitAndInstall(), 100));
autoUpdater.on("download-progress", p =>
updaterWindow?.webContents.send(UpdaterIpcEvents.DOWNLOAD_PROGRESS, p.percent)
);
autoUpdater.on("error", err => updaterWindow?.webContents.send(UpdaterIpcEvents.ERROR, err.message));
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.fullChangelog = true;
const isOutdated = autoUpdater.checkForUpdates().then(res => Boolean(res?.isUpdateAvailable));
handle(IpcEvents.UPDATER_IS_OUTDATED, () => isOutdated);
handle(IpcEvents.UPDATER_OPEN, async () => {
const res = await autoUpdater.checkForUpdates();
if (res?.isUpdateAvailable && res.updateInfo) openUpdater(res.updateInfo);
});
function openUpdater(update: UpdateInfo) {
updaterWindow = new BrowserWindow({
title: "Vesktop Updater",
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, "updaterPreload.js")
},
minHeight: 400,
minWidth: 750
});
makeLinksOpenExternally(updaterWindow);
handle(UpdaterIpcEvents.GET_DATA, () => ({ update, version: app.getVersion() }));
handle(UpdaterIpcEvents.INSTALL, async () => {
await autoUpdater.downloadUpdate();
});
handle(UpdaterIpcEvents.SNOOZE_UPDATE, () => {
State.store.updater ??= {};
State.store.updater.snoozeUntil = Date.now() + 1 * Millis.DAY;
updaterWindow?.close();
});
handle(UpdaterIpcEvents.IGNORE_UPDATE, () => {
State.store.updater ??= {};
State.store.updater.ignoredVersion = update.version;
updaterWindow?.close();
});
updaterWindow.on("closed", () => {
ipcMain.removeHandler(UpdaterIpcEvents.GET_DATA);
ipcMain.removeHandler(UpdaterIpcEvents.INSTALL);
ipcMain.removeHandler(UpdaterIpcEvents.SNOOZE_UPDATE);
ipcMain.removeHandler(UpdaterIpcEvents.IGNORE_UPDATE);
updaterWindow = null;
});
loadView(updaterWindow, "updater/index.html");
}

View File

@@ -1,101 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, dialog, net } from "electron";
import { copyFile, mkdir, rm } from "fs/promises";
import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
import { STATIC_DIR } from "shared/paths";
import { pathToFileURL } from "url";
import { DATA_DIR } from "./constants";
import { AppEvents } from "./events";
import { mainWin } from "./mainWindow";
import { fileExistsAsync } from "./utils/fileExists";
import { handle } from "./utils/ipcWrappers";
const CUSTOMIZABLE_ASSETS = ["splash", "tray", "trayUnread"] as const;
export type UserAssetType = (typeof CUSTOMIZABLE_ASSETS)[number];
const DEFAULT_ASSETS: Record<UserAssetType, string> = {
splash: "splash.webp",
tray: `tray/${process.platform === "darwin" ? "trayTemplate" : "tray"}.png`,
trayUnread: "tray/trayUnread.png"
};
const UserAssetFolder = join(DATA_DIR, "userAssets");
export async function resolveAssetPath(asset: UserAssetType) {
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
throw new Error(`Invalid asset: ${asset}`);
}
const assetPath = join(UserAssetFolder, asset);
if (await fileExistsAsync(assetPath)) {
return assetPath;
}
return join(STATIC_DIR, DEFAULT_ASSETS[asset]);
}
export async function handleVesktopAssetsProtocol(path: string, req: Request) {
const asset = path.slice(1);
// @ts-expect-error dumb types
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
return new Response(null, { status: 404 });
}
try {
const res = await net.fetch(pathToFileURL(join(UserAssetFolder, asset)).href);
if (res.ok) return res;
} catch {}
return net.fetch(pathToFileURL(join(STATIC_DIR, DEFAULT_ASSETS[asset])).href);
}
handle(IpcEvents.CHOOSE_USER_ASSET, async (_event, asset: UserAssetType, value?: null) => {
if (!CUSTOMIZABLE_ASSETS.includes(asset)) {
throw `Invalid asset: ${asset}`;
}
const assetPath = join(UserAssetFolder, asset);
if (value === null) {
try {
await rm(assetPath, { force: true });
AppEvents.emit("userAssetChanged", asset);
return "ok";
} catch (e) {
console.error(`Failed to remove user asset ${asset}:`, e);
return "failed";
}
}
const res = await dialog.showOpenDialog(mainWin, {
properties: ["openFile"],
title: `Select an image to use as ${asset}`,
defaultPath: app.getPath("pictures"),
filters: [
{
name: "Images",
extensions: ["png", "jpg", "jpeg", "webp", "gif", "avif", "svg"]
}
]
});
if (res.canceled || !res.filePaths.length) return "cancelled";
try {
await mkdir(UserAssetFolder, { recursive: true });
await copyFile(res.filePaths[0], assetPath);
AppEvents.emit("userAssetChanged", asset);
return "ok";
} catch (e) {
console.error(`Failed to copy user asset ${asset}:`, e);
return "failed";
}
});

View File

@@ -1,32 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, BrowserWindow, dialog } from "electron";
import { rm } from "fs/promises";
import { DATA_DIR, MessageBoxChoice } from "main/constants";
export async function clearData(win: BrowserWindow) {
const { response } = await dialog.showMessageBox(win, {
message: "Are you sure you want to reset Vesktop?",
detail: "This will log you out, clear caches and reset all your settings!\n\nVesktop will automatically restart after this operation.",
buttons: ["Yes", "No"],
cancelId: MessageBoxChoice.Cancel,
defaultId: MessageBoxChoice.Default,
type: "warning"
});
if (response === MessageBoxChoice.Cancel) return;
win.close();
await win.webContents.session.clearStorageData();
await win.webContents.session.clearCache();
await win.webContents.session.clearCodeCaches({});
await rm(DATA_DIR, { force: true, recursive: true });
app.relaunch();
app.quit();
}

View File

@@ -1,56 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html
// "If an argument contains a reserved character the argument must be quoted."
const desktopFileReservedChars = new Set([
" ",
"\t",
"\n",
'"',
"'",
"\\",
">",
"<",
"~",
"|",
"&",
";",
"$",
"*",
"?",
"#",
"(",
")",
"`"
]);
export function escapeDesktopFileArgument(arg: string) {
let needsQuoting = false;
let out = "";
for (const c of arg) {
if (desktopFileReservedChars.has(c)) {
// "Quoting must be done by enclosing the argument between double quotes"
needsQuoting = true;
// "and escaping the double quote character, backtick character ("`"), dollar sign ("$")
// and backslash character ("\") by preceding it with an additional backslash character"
if (c === '"' || c === "`" || c === "$" || c === "\\") {
out += "\\";
}
}
// "Literal percentage characters must be escaped as %%"
if (c === "%") {
out += "%%";
} else {
out += c;
}
}
return needsQuoting ? `"${out}"` : out;
}

View File

@@ -1,13 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { access, constants } from "fs/promises";
export async function fileExistsAsync(path: string) {
return await access(path, constants.F_OK)
.then(() => true)
.catch(() => false);
}

View File

@@ -1,58 +1,45 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { createWriteStream } from "fs"; import { createWriteStream } from "fs";
import { Readable } from "stream"; import type { IncomingMessage } from "http";
import { pipeline } from "stream/promises"; import { get, RequestOptions } from "https";
import { setTimeout } from "timers/promises"; import { finished } from "stream/promises";
interface FetchieOptions { export async function downloadFile(url: string, file: string, options: RequestOptions = {}) {
retryOnNetworkError?: boolean; const res = await simpleReq(url, options);
} await finished(
res.pipe(
export async function downloadFile(url: string, file: string, options: RequestInit = {}, fetchieOpts?: FetchieOptions) { createWriteStream(file, {
const res = await fetchie(url, options, fetchieOpts); autoClose: true
await pipeline( })
// @ts-expect-error odd type error )
Readable.fromWeb(res.body!),
createWriteStream(file, {
autoClose: true
})
); );
} }
const ONE_MINUTE_MS = 1000 * 60; export function simpleReq(url: string, options: RequestOptions = {}) {
return new Promise<IncomingMessage>((resolve, reject) => {
get(url, options, res => {
const { statusCode, statusMessage, headers } = res;
if (statusCode! >= 400) return void reject(`${statusCode}: ${statusMessage} - ${url}`);
if (statusCode! >= 300) return simpleReq(headers.location!, options).then(resolve).catch(reject);
export async function fetchie(url: string, options?: RequestInit, { retryOnNetworkError }: FetchieOptions = {}) { resolve(res);
let res: Response | undefined; });
});
try { }
res = await fetch(url, options);
} catch (err) { export async function simpleGet(url: string, options: RequestOptions = {}) {
if (retryOnNetworkError) { const res = await simpleReq(url, options);
console.error("Failed to fetch", url + ".", "Gonna retry with backoff.");
return new Promise<Buffer>((resolve, reject) => {
for (let tries = 0, delayMs = 500; tries < 20; tries++, delayMs = Math.min(2 * delayMs, ONE_MINUTE_MS)) { const chunks = [] as Buffer[];
await setTimeout(delayMs);
try { res.once("error", reject);
res = await fetch(url, options); res.on("data", chunk => chunks.push(chunk));
break; res.once("end", () => resolve(Buffer.concat(chunks)));
} catch {} });
}
}
if (!res) throw new Error(`Failed to fetch ${url}\n${err}`);
}
if (res.ok) return res;
let msg = `Got non-OK response for ${url}: ${res.status} ${res.statusText}`;
const reason = await res.text().catch(() => "");
if (reason) msg += `\n${reason}`;
throw new Error(msg);
} }

View File

@@ -1,40 +1,36 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron"; import { ipcMain, IpcMainEvent, IpcMainInvokeEvent, WebFrameMain } from "electron";
import { DISCORD_HOSTNAMES } from "main/constants"; import { IpcEvents } from "shared/IpcEvents";
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents";
export function validateSender(frame: WebFrameMain | null, event: string) { export function validateSender(frame: WebFrameMain) {
if (!frame) throw new Error(`ipc[${event}]: No sender frame`); const { hostname, protocol } = new URL(frame.url);
if (!frame.url) return; if (protocol === "file:") return;
try { switch (hostname) {
var { hostname, protocol } = new URL(frame.url); case "discord.com":
} catch (e) { case "ptb.discord.com":
throw new Error(`ipc[${event}]: Invalid URL ${frame.url}`); case "canary.discord.com":
} break;
default:
if (protocol === "file:" || protocol === "vesktop:") return; throw new Error("ipc: Disallowed host " + hostname);
if (!DISCORD_HOSTNAMES.includes(hostname)) {
throw new Error(`ipc[${event}]: Disallowed hostname ${hostname}`);
} }
} }
export function handleSync(event: IpcEvents | UpdaterIpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) { export function handleSync(event: IpcEvents, cb: (e: IpcMainEvent, ...args: any[]) => any) {
ipcMain.on(event, (e, ...args) => { ipcMain.on(event, (e, ...args) => {
validateSender(e.senderFrame, event); validateSender(e.senderFrame);
e.returnValue = cb(e, ...args); e.returnValue = cb(e, ...args);
}); });
} }
export function handle(event: IpcEvents | UpdaterIpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) { export function handle(event: IpcEvents, cb: (e: IpcMainInvokeEvent, ...args: any[]) => any) {
ipcMain.handle(event, (e, ...args) => { ipcMain.handle(event, (e, ...args) => {
validateSender(e.senderFrame, event); validateSender(e.senderFrame);
return cb(e, ...args); return cb(e, ...args);
}); });
} }

View File

@@ -1,16 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { resolve, sep } from "path";
export function isPathInDirectory(filePath: string, directory: string) {
const resolvedPath = resolve(filePath);
const resolvedDirectory = resolve(directory);
const normalizedDirectory = resolvedDirectory.endsWith(sep) ? resolvedDirectory : resolvedDirectory + sep;
return resolvedPath.startsWith(normalizedDirectory) || resolvedPath === resolvedDirectory;
}

View File

@@ -1,73 +1,40 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { BrowserWindow, shell } from "electron"; import { BrowserWindow, shell } from "electron";
import { DISCORD_HOSTNAMES } from "main/constants";
import { Settings } from "../settings"; import { Settings } from "../settings";
import { createOrFocusPopup, setupPopout } from "./popout";
import { execSteamURL, isDeckGameMode, steamOpenURL } from "./steamOS";
export function handleExternalUrl(url: string, protocol?: string): { action: "deny" | "allow" } {
if (protocol == null) {
try {
protocol = new URL(url).protocol;
} catch {
return { action: "deny" };
}
}
switch (protocol) {
case "http:":
case "https:":
if (Settings.store.openLinksWithElectron) {
return { action: "allow" };
}
// eslint-disable-next-line no-fallthrough
case "mailto:":
case "spotify:":
if (isDeckGameMode) {
steamOpenURL(url);
} else {
shell.openExternal(url);
}
break;
case "steam:":
if (isDeckGameMode) {
execSteamURL(url);
} else {
shell.openExternal(url);
}
break;
}
return { action: "deny" };
}
export function makeLinksOpenExternally(win: BrowserWindow) { export function makeLinksOpenExternally(win: BrowserWindow) {
win.webContents.setWindowOpenHandler(({ url, frameName, features }) => { win.webContents.setWindowOpenHandler(({ url }) => {
switch (url) {
case "about:blank":
case "https://discord.com/popout":
return { action: "allow" };
}
try { try {
var { protocol, hostname, pathname, searchParams } = new URL(url); var { protocol } = new URL(url);
} catch { } catch {
return { action: "deny" }; return { action: "deny" };
} }
if (frameName.startsWith("DISCORD_") && pathname === "/popout" && DISCORD_HOSTNAMES.includes(hostname)) { switch (protocol) {
return createOrFocusPopup(frameName, features); case "http:":
case "https:":
if (Settings.store.openLinksWithElectron) {
return { action: "allow" };
}
// eslint-disable-next-line no-fallthrough
case "mailto:":
case "steam:":
case "spotify:":
shell.openExternal(url);
} }
if (url === "about:blank") return { action: "allow" }; return { action: "deny" };
// Drop the static temp page Discord web loads for the connections popout
if (frameName === "authorize" && searchParams.get("loading") === "true") return { action: "deny" };
return handleExternalUrl(url, protocol);
});
win.webContents.on("did-create-window", (win, { frameName }) => {
if (frameName.startsWith("DISCORD_")) setupPopout(win, frameName);
}); });
} }

View File

@@ -1,116 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
import { Settings } from "main/settings";
import { handleExternalUrl } from "./makeLinksOpenExternally";
const ALLOWED_FEATURES = new Set([
"width",
"height",
"left",
"top",
"resizable",
"movable",
"alwaysOnTop",
"frame",
"transparent",
"hasShadow",
"closable",
"skipTaskbar",
"backgroundColor",
"menubar",
"toolbar",
"location",
"directories",
"titleBarStyle"
]);
const MIN_POPOUT_WIDTH = 320;
const MIN_POPOUT_HEIGHT = 180;
const DEFAULT_POPOUT_OPTIONS: BrowserWindowConstructorOptions = {
title: "Discord Popout",
backgroundColor: "#2f3136",
minWidth: MIN_POPOUT_WIDTH,
minHeight: MIN_POPOUT_HEIGHT,
frame: Settings.store.customTitleBar !== true,
titleBarStyle: process.platform === "darwin" ? "hidden" : undefined,
trafficLightPosition:
process.platform === "darwin"
? {
x: 10,
y: 3
}
: undefined,
webPreferences: {
nodeIntegration: false,
contextIsolation: true
},
autoHideMenuBar: Settings.store.enableMenu
};
export const PopoutWindows = new Map<string, BrowserWindow>();
function focusWindow(window: BrowserWindow) {
window.setAlwaysOnTop(true);
window.focus();
window.setAlwaysOnTop(false);
}
function parseFeatureValue(feature: string) {
if (feature === "yes") return true;
if (feature === "no") return false;
const n = Number(feature);
if (!isNaN(n)) return n;
return feature;
}
function parseWindowFeatures(features: string) {
const keyValuesParsed = features.split(",");
return keyValuesParsed.reduce((features, feature) => {
const [key, value] = feature.split("=");
if (ALLOWED_FEATURES.has(key)) features[key] = parseFeatureValue(value);
return features;
}, {});
}
export function createOrFocusPopup(key: string, features: string) {
const existingWindow = PopoutWindows.get(key);
if (existingWindow) {
focusWindow(existingWindow);
return <const>{ action: "deny" };
}
return <const>{
action: "allow",
overrideBrowserWindowOptions: {
...DEFAULT_POPOUT_OPTIONS,
...parseWindowFeatures(features)
}
};
}
export function setupPopout(win: BrowserWindow, key: string) {
win.setMenuBarVisibility(false);
PopoutWindows.set(key, win);
/* win.webContents.on("will-navigate", (evt, url) => {
// maybe prevent if not origin match
})*/
win.webContents.setWindowOpenHandler(({ url }) => handleExternalUrl(url));
win.once("closed", () => {
win.removeAllListeners();
PopoutWindows.delete(key);
});
}

View File

@@ -1,29 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { execFile } from "child_process";
import { app } from "electron";
export async function setAsDefaultProtocolClient(protocol: string) {
if (process.platform !== "linux") {
return app.setAsDefaultProtocolClient(protocol);
}
// electron setAsDefaultProtocolClient uses xdg-settings instead of xdg-mime.
// xdg-settings had a bug where it would also register the app as a handler for text/html,
// aka become your browser. This bug was fixed years ago (xdg-utils 1.2.0) but Ubuntu ships
// 7 (YES, SEVEN) years out of date xdg-utils which STILL has the bug.
// FIXME: remove this workaround when Ubuntu updates their xdg-utils or electron switches to xdg-mime.
const { CHROME_DESKTOP } = process.env;
if (!CHROME_DESKTOP) return false;
return new Promise<boolean>(resolve => {
execFile("xdg-mime", ["default", CHROME_DESKTOP, `x-scheme-handler/${protocol}`], err => {
resolve(err == null);
});
});
}

View File

@@ -1,15 +1,18 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { exec as callbackExec } from "child_process";
import { BrowserWindow, dialog } from "electron"; import { BrowserWindow, dialog } from "electron";
import { writeFile } from "fs/promises"; import { sleep } from "shared/utils/sleep";
import { join } from "path"; import { promisify } from "util";
import { MessageBoxChoice } from "../constants"; import { MessageBoxChoice } from "../constants";
import { State } from "../settings"; import { Settings } from "../settings";
const exec = promisify(callbackExec);
// Bump this to re-show the prompt // Bump this to re-show the prompt
const layoutVersion = 2; const layoutVersion = 2;
@@ -17,8 +20,6 @@ const layoutVersion = 2;
const layoutId = "3080264545"; // Vesktop Layout v2 const layoutId = "3080264545"; // Vesktop Layout v2
const numberRegex = /^[0-9]*$/; const numberRegex = /^[0-9]*$/;
let steamPipeQueue = Promise.resolve();
export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1"; export const isDeckGameMode = process.env.SteamOS === "1" && process.env.SteamGamepadUI === "1";
export function applyDeckKeyboardFix() { export function applyDeckKeyboardFix() {
@@ -41,37 +42,23 @@ function getAppId(): string | null {
return null; return null;
} }
export function execSteamURL(url: string) { async function execSteamURL(url: string): Promise<void> {
// This doesn't allow arbitrary execution despite the weird syntax. await exec(`steam -ifrunning ${url}`);
steamPipeQueue = steamPipeQueue.then(() =>
writeFile(
join(process.env.HOME || "/home/deck", ".steam", "steam.pipe"),
// replace ' to prevent argument injection
`'${process.env.HOME}/.local/share/Steam/ubuntu12_32/steam' '-ifrunning' '${url.replaceAll("'", "%27")}'\n`,
"utf-8"
)
);
}
export function steamOpenURL(url: string) {
execSteamURL(`steam://openurl/${url}`);
}
export async function showGamePage() {
const appId = getAppId();
if (!appId) return;
await execSteamURL(`steam://nav/games/details/${appId}`);
} }
async function showLayout(appId: string) { async function showLayout(appId: string) {
execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`); await execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`);
// because the UI doesn't consistently reload after the data for the config has loaded...
// HOW HAS NOBODY AT VALVE RUN INTO THIS YET
await sleep(100);
await execSteamURL(`steam://controllerconfig/${appId}/${layoutId}`);
} }
export async function askToApplySteamLayout(win: BrowserWindow) { export async function askToApplySteamLayout(win: BrowserWindow) {
const appId = getAppId(); const appId = getAppId();
if (!appId) return; if (!appId) return;
if (State.store.steamOSLayoutVersion === layoutVersion) return; if (Settings.store.steamOSLayoutVersion === layoutVersion) return;
const update = Boolean(State.store.steamOSLayoutVersion); const update = Boolean(Settings.store.steamOSLayoutVersion);
// 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. // 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.
const { response } = await dialog.showMessageBox(win, { const { response } = await dialog.showMessageBox(win, {
@@ -87,8 +74,8 @@ ${update ? "Click" : "Tap"} no to keep your current layout.`,
type: "question" type: "question"
}); });
if (State.store.steamOSLayoutVersion !== layoutVersion) { if (Settings.store.steamOSLayoutVersion !== layoutVersion) {
State.store.steamOSLayoutVersion = layoutVersion; Settings.store.steamOSLayoutVersion = layoutVersion;
} }
if (response === MessageBoxChoice.Cancel) return; if (response === MessageBoxChoice.Cancel) return;

View File

@@ -1,16 +1,15 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { mkdirSync } from "fs"; import { existsSync, mkdirSync } from "fs";
import { access, constants as FsConstants, writeFile } from "fs/promises"; import type { RequestOptions } from "https";
import { VENCORD_FILES_DIR } from "main/vencordFilesDir";
import { join } from "path"; import { join } from "path";
import { USER_AGENT } from "../constants"; import { USER_AGENT, VENCORD_FILES_DIR } from "../constants";
import { downloadFile, fetchie } from "./http"; import { downloadFile, simpleGet } from "./http";
const API_BASE = "https://api.github.com"; const API_BASE = "https://api.github.com";
@@ -32,46 +31,37 @@ export interface ReleaseData {
} }
export async function githubGet(endpoint: string) { export async function githubGet(endpoint: string) {
const opts: RequestInit = { const opts: RequestOptions = {
headers: { headers: {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
"User-Agent": USER_AGENT "User-Agent": USER_AGENT
} }
}; };
if (process.env.GITHUB_TOKEN) (opts.headers! as any).Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; if (process.env.GITHUB_TOKEN) opts.headers!.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
return fetchie(API_BASE + endpoint, opts, { retryOnNetworkError: true }); return simpleGet(API_BASE + endpoint, opts);
} }
export async function downloadVencordFiles() { export async function downloadVencordFiles() {
const release = await githubGet("/repos/Vendicated/Vencord/releases/latest"); const release = await githubGet("/repos/Vendicated/Vencord/releases/latest");
const { assets }: ReleaseData = await release.json(); const { assets } = JSON.parse(release.toString("utf-8")) as ReleaseData;
await Promise.all( await Promise.all(
assets assets
.filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f))) .filter(({ name }) => FILES_TO_DOWNLOAD.some(f => name.startsWith(f)))
.map(({ name, browser_download_url }) => .map(({ name, browser_download_url }) => downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name)))
downloadFile(browser_download_url, join(VENCORD_FILES_DIR, name), {}, { retryOnNetworkError: true })
)
); );
} }
const existsAsync = (path: string) => export function isValidVencordInstall(dir: string) {
access(path, FsConstants.F_OK) return FILES_TO_DOWNLOAD.every(f => existsSync(join(dir, f)));
.then(() => true)
.catch(() => false);
export async function isValidVencordInstall(dir: string) {
const results = await Promise.all(["package.json", ...FILES_TO_DOWNLOAD].map(f => existsAsync(join(dir, f))));
return !results.includes(false);
} }
export async function ensureVencordFiles() { export async function ensureVencordFiles() {
if (await isValidVencordInstall(VENCORD_FILES_DIR)) return; if (isValidVencordInstall(VENCORD_FILES_DIR)) return;
mkdirSync(VENCORD_FILES_DIR, { recursive: true }); mkdirSync(VENCORD_FILES_DIR, { recursive: true });
await Promise.all([downloadVencordFiles(), writeFile(join(VENCORD_FILES_DIR, "package.json"), "{}")]); await downloadVencordFiles();
} }

View File

@@ -1,13 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { join } from "path";
import { SESSION_DATA_DIR } from "./constants";
import { State } from "./settings";
// this is in a separate file to avoid circular dependencies
export const VENCORD_FILES_DIR = State.store.vencordDir || join(SESSION_DATA_DIR, "vencordFiles");

View File

@@ -1,135 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type { LinkData, Node, PatchBay as PatchBayType } from "@vencord/venmic";
import { app, ipcMain } from "electron";
import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
import { STATIC_DIR } from "shared/paths";
import { Settings } from "./settings";
let PatchBay: typeof PatchBayType | undefined;
let patchBayInstance: PatchBayType | undefined;
let imported = false;
let initialized = false;
let hasPipewirePulse = false;
let isGlibCxxOutdated = false;
function importVenmic() {
if (imported) {
return;
}
imported = true;
try {
PatchBay = (require(join(STATIC_DIR, `dist/venmic-${process.arch}.node`)) as typeof import("@vencord/venmic"))
.PatchBay;
hasPipewirePulse = PatchBay.hasPipeWire();
} catch (e: any) {
console.error("Failed to import venmic", e);
isGlibCxxOutdated = (e?.stack || e?.message || "").toLowerCase().includes("glibc");
}
}
function obtainVenmic() {
if (!imported) {
importVenmic();
}
if (PatchBay && !initialized) {
initialized = true;
try {
patchBayInstance = new PatchBay();
} catch (e: any) {
console.error("Failed to instantiate venmic", e);
}
}
return patchBayInstance;
}
function getRendererAudioServicePid() {
return (
app
.getAppMetrics()
.find(proc => proc.name === "Audio Service")
?.pid?.toString() ?? "owo"
);
}
ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
const audioPid = getRendererAudioServicePid();
const { granularSelect } = Settings.store.audio ?? {};
const targets = obtainVenmic()
?.list(granularSelect ? ["node.name"] : undefined)
.filter(s => s["application.process.id"] !== audioPid);
return targets ? { ok: true, targets, hasPipewirePulse } : { ok: false, isGlibCxxOutdated };
});
ipcMain.handle(IpcEvents.VIRT_MIC_START, (_, include: Node[]) => {
const pid = getRendererAudioServicePid();
const { ignoreDevices, ignoreInputMedia, ignoreVirtual, workaround } = Settings.store.audio ?? {};
const data: LinkData = {
include,
exclude: [{ "application.process.id": pid }],
ignore_devices: ignoreDevices
};
if (ignoreInputMedia ?? true) {
data.exclude.push({ "media.class": "Stream/Input/Audio" });
}
if (ignoreVirtual) {
data.exclude.push({ "node.virtual": "true" });
}
if (workaround) {
data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
}
return obtainVenmic()?.link(data);
});
ipcMain.handle(IpcEvents.VIRT_MIC_START_SYSTEM, (_, exclude: Node[]) => {
const pid = getRendererAudioServicePid();
const { workaround, ignoreDevices, ignoreInputMedia, ignoreVirtual, onlySpeakers, onlyDefaultSpeakers } =
Settings.store.audio ?? {};
const data: LinkData = {
include: [],
exclude: [{ "application.process.id": pid }, ...exclude],
only_speakers: onlySpeakers,
ignore_devices: ignoreDevices,
only_default_speakers: onlyDefaultSpeakers
};
if (ignoreInputMedia ?? true) {
data.exclude.push({ "media.class": "Stream/Input/Audio" });
}
if (ignoreVirtual) {
data.exclude.push({ "node.virtual": "true" });
}
if (workaround) {
data.workaround = [{ "application.process.id": pid, "media.name": "RecordStream" }];
}
return obtainVenmic()?.link(data);
});
ipcMain.handle(IpcEvents.VIRT_MIC_STOP, () => obtainVenmic()?.unlink());

View File

@@ -1,25 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { app, protocol } from "electron";
import { handleVesktopAssetsProtocol } from "./userAssets";
import { handleVesktopStaticProtocol } from "./vesktopStatic";
app.whenReady().then(() => {
protocol.handle("vesktop", async req => {
const url = new URL(req.url);
switch (url.hostname) {
case "assets":
return handleVesktopAssetsProtocol(url.pathname, req);
case "static":
return handleVesktopStaticProtocol(url.pathname, req);
default:
return new Response(null, { status: 404 });
}
});
});

View File

@@ -1,31 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BrowserWindow, net } from "electron";
import { join } from "path";
import { pathToFileURL } from "url";
import { isPathInDirectory } from "./utils/isPathInDirectory";
const STATIC_DIR = join(__dirname, "..", "..", "static");
export async function handleVesktopStaticProtocol(path: string, req: Request) {
const fullPath = join(STATIC_DIR, path);
if (!isPathInDirectory(fullPath, STATIC_DIR)) {
return new Response(null, { status: 404 });
}
return net.fetch(pathToFileURL(fullPath).href);
}
export function loadView(browserWindow: BrowserWindow, view: string, params?: URLSearchParams) {
const url = new URL(`vesktop://static/views/${view}`);
if (params) {
url.search = params.toString();
}
return browserWindow.loadURL(url.toString());
}

77
src/main/virtmic.ts Normal file
View File

@@ -0,0 +1,77 @@
/*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors
*/
import { app, ipcMain } from "electron";
import { join } from "path";
import { IpcEvents } from "shared/IpcEvents";
import { STATIC_DIR } from "shared/paths";
let initialized = false;
let patchBay: import("@vencord/venmic").PatchBay | undefined;
let isGlibcxxToOld = false;
function getRendererAudioServicePid() {
return (
app
.getAppMetrics()
.find(proc => proc.name === "Audio Service")
?.pid?.toString() ?? "owo"
);
}
function obtainVenmic() {
if (!initialized) {
initialized = true;
try {
const { PatchBay } = require(
join(STATIC_DIR, `dist/venmic-${process.arch}.node`)
) as typeof import("@vencord/venmic");
patchBay = new PatchBay();
} catch (e: any) {
console.error("Failed to initialise venmic. Make sure you're using pipewire", e);
isGlibcxxToOld = (e?.stack || e?.message || "").toLowerCase().includes("glibc");
}
}
return patchBay;
}
ipcMain.handle(IpcEvents.VIRT_MIC_LIST, () => {
const audioPid = getRendererAudioServicePid();
const list = obtainVenmic()
?.list()
.filter(s => s["application.process.id"] !== audioPid)
.map(s => s["application.name"]);
return list
? { ok: true, targets: [...new Set(list)] } // Remove duplicates
: { ok: false, isGlibcxxToOld };
});
ipcMain.handle(
IpcEvents.VIRT_MIC_START,
(_, targets: string[]) =>
obtainVenmic()?.link({
props: targets.map(target => ({ key: "application.name", value: target })),
mode: "include"
})
);
ipcMain.handle(
IpcEvents.VIRT_MIC_START_SYSTEM,
() =>
obtainVenmic()?.link({
props: [
{
key: "application.process.id",
value: getRendererAudioServicePid()
}
],
mode: "exclude"
})
);
ipcMain.handle(IpcEvents.VIRT_MIC_STOP, () => obtainVenmic()?.unlink());

10
src/module.d.ts vendored
View File

@@ -1,10 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
declare module "__patches__" {
const never: never;
export default never;
}

View File

@@ -1,13 +1,12 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { Node } from "@vencord/venmic";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { IpcMessage, IpcResponse } from "main/ipcCommands";
import type { Settings } from "shared/settings"; import type { Settings } from "shared/settings";
import type { LiteralUnion } from "type-fest";
import { IpcEvents } from "../shared/IpcEvents"; import { IpcEvents } from "../shared/IpcEvents";
import { invoke, sendSync } from "./typedIpc"; import { invoke, sendSync } from "./typedIpc";
@@ -20,21 +19,12 @@ ipcRenderer.on(IpcEvents.SPELLCHECK_RESULT, (_, w: string, s: string[]) => {
spellCheckCallbacks.forEach(cb => cb(w, s)); spellCheckCallbacks.forEach(cb => cb(w, s));
}); });
let onDevtoolsOpen = () => {};
let onDevtoolsClose = () => {};
ipcRenderer.on(IpcEvents.DEVTOOLS_OPENED, () => onDevtoolsOpen());
ipcRenderer.on(IpcEvents.DEVTOOLS_CLOSED, () => onDevtoolsClose());
export const VesktopNative = { export const VesktopNative = {
app: { app: {
relaunch: () => invoke<void>(IpcEvents.RELAUNCH), relaunch: () => invoke<void>(IpcEvents.RELAUNCH),
getVersion: () => sendSync<void>(IpcEvents.GET_VERSION), getVersion: () => sendSync<void>(IpcEvents.GET_VERSION),
setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count), setBadgeCount: (count: number) => invoke<void>(IpcEvents.SET_BADGE_COUNT, count),
supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY), supportsWindowsTransparency: () => sendSync<boolean>(IpcEvents.SUPPORTS_WINDOWS_TRANSPARENCY)
getEnableHardwareAcceleration: () => sendSync<boolean>(IpcEvents.GET_ENABLE_HARDWARE_ACCELERATION),
isOutdated: () => invoke<boolean>(IpcEvents.UPDATER_IS_OUTDATED),
openUpdater: () => invoke<void>(IpcEvents.UPDATER_OPEN)
}, },
autostart: { autostart: {
isEnabled: () => sendSync<boolean>(IpcEvents.AUTOSTART_ENABLED), isEnabled: () => sendSync<boolean>(IpcEvents.AUTOSTART_ENABLED),
@@ -43,17 +33,14 @@ export const VesktopNative = {
}, },
fileManager: { fileManager: {
showItemInFolder: (path: string) => invoke<void>(IpcEvents.SHOW_ITEM_IN_FOLDER, path), showItemInFolder: (path: string) => invoke<void>(IpcEvents.SHOW_ITEM_IN_FOLDER, path),
getVencordDir: () => sendSync<string | undefined>(IpcEvents.GET_VENCORD_DIR), selectVencordDir: () => invoke<LiteralUnion<"cancelled" | "invalid", string>>(IpcEvents.SELECT_VENCORD_DIR)
selectVencordDir: (value?: null) => invoke<"cancelled" | "invalid" | "ok">(IpcEvents.SELECT_VENCORD_DIR, value),
chooseUserAsset: (asset: string, value?: null) =>
invoke<"cancelled" | "invalid" | "ok" | "failed">(IpcEvents.CHOOSE_USER_ASSET, asset, value)
}, },
settings: { settings: {
get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS), get: () => sendSync<Settings>(IpcEvents.GET_SETTINGS),
set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path) set: (settings: Settings, path?: string) => invoke<void>(IpcEvents.SET_SETTINGS, settings, path)
}, },
spellcheck: { spellcheck: {
getAvailableLanguages: () => sendSync<string[]>(IpcEvents.SPELLCHECK_GET_AVAILABLE_LANGUAGES), setLanguages: (languages: readonly string[]) => invoke<void>(IpcEvents.SPELLCHECK_SET_LANGUAGES, languages),
onSpellcheckResult(cb: SpellCheckerResultCallback) { onSpellcheckResult(cb: SpellCheckerResultCallback) {
spellCheckCallbacks.add(cb); spellCheckCallbacks.add(cb);
}, },
@@ -65,14 +52,9 @@ export const VesktopNative = {
}, },
win: { win: {
focus: () => invoke<void>(IpcEvents.FOCUS), focus: () => invoke<void>(IpcEvents.FOCUS),
close: (key?: string) => invoke<void>(IpcEvents.CLOSE, key), close: () => invoke<void>(IpcEvents.CLOSE),
minimize: (key?: string) => invoke<void>(IpcEvents.MINIMIZE, key), minimize: () => invoke<void>(IpcEvents.MINIMIZE),
maximize: (key?: string) => invoke<void>(IpcEvents.MAXIMIZE, key), maximize: () => invoke<void>(IpcEvents.MAXIMIZE)
flashFrame: (flag: boolean) => invoke<void>(IpcEvents.FLASH_FRAME, flag),
setDevtoolsCallbacks: (onOpen: () => void, onClose: () => void) => {
onDevtoolsOpen = onOpen;
onDevtoolsClose = onClose;
}
}, },
capturer: { capturer: {
getLargeThumbnail: (id: string) => invoke<string>(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id) getLargeThumbnail: (id: string) => invoke<string>(IpcEvents.CAPTURER_GET_LARGE_THUMBNAIL, id)
@@ -80,25 +62,14 @@ export const VesktopNative = {
/** only available on Linux. */ /** only available on Linux. */
virtmic: { virtmic: {
list: () => list: () =>
invoke< invoke<{ ok: false; isGlibcxxToOld: boolean } | { ok: true; targets: string[] }>(IpcEvents.VIRT_MIC_LIST),
{ ok: false; isGlibCxxOutdated: boolean } | { ok: true; targets: Node[]; hasPipewirePulse: boolean } start: (targets: string[]) => invoke<void>(IpcEvents.VIRT_MIC_START, targets),
>(IpcEvents.VIRT_MIC_LIST), startSystem: () => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM),
start: (include: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START, include),
startSystem: (exclude: Node[]) => invoke<void>(IpcEvents.VIRT_MIC_START_SYSTEM, exclude),
stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP) stop: () => invoke<void>(IpcEvents.VIRT_MIC_STOP)
}, },
clipboard: { arrpc: {
copyImage: (imageBuffer: Uint8Array, imageSrc: string) => onActivity(cb: (data: string) => void) {
invoke<void>(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc) ipcRenderer.on(IpcEvents.ARRPC_ACTIVITY, (_, data: string) => cb(data));
}, }
debug: {
launchGpu: () => invoke<void>(IpcEvents.DEBUG_LAUNCH_GPU),
launchWebrtcInternals: () => invoke<void>(IpcEvents.DEBUG_LAUNCH_WEBRTC_INTERNALS)
},
commands: {
onCommand(cb: (message: IpcMessage) => void) {
ipcRenderer.on(IpcEvents.IPC_COMMAND, (_, message) => cb(message));
},
respond: (response: IpcResponse) => ipcRenderer.send(IpcEvents.IPC_COMMAND, response)
} }
}; };

View File

@@ -1,7 +1,7 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { contextBridge, ipcRenderer, webFrame } from "electron"; import { contextBridge, ipcRenderer, webFrame } from "electron";
@@ -40,3 +40,5 @@ if (IS_DEV) {
}); });
} }
// #endregion // #endregion
VesktopNative.spellcheck.setLanguages(window.navigator.languages);

View File

@@ -1,13 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("VesktopSplashNative", {
onUpdateMessage(callback: (message: string) => void) {
ipcRenderer.on("update-splash-message", (_, message: string) => callback(message));
}
});

View File

@@ -1,16 +1,16 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { IpcEvents, UpdaterIpcEvents } from "shared/IpcEvents"; import { IpcEvents } from "shared/IpcEvents";
export function invoke<T = any>(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) { export function invoke<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.invoke(event, ...args) as Promise<T>; return ipcRenderer.invoke(event, ...args) as Promise<T>;
} }
export function sendSync<T = any>(event: IpcEvents | UpdaterIpcEvents, ...args: any[]) { export function sendSync<T = any>(event: IpcEvents, ...args: any[]) {
return ipcRenderer.sendSync(event, ...args) as T; return ipcRenderer.sendSync(event, ...args) as T;
} }

View File

@@ -1,24 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { contextBridge, ipcRenderer } from "electron";
import type { UpdateInfo } from "electron-updater";
import { UpdaterIpcEvents } from "shared/IpcEvents";
import { invoke } from "./typedIpc";
contextBridge.exposeInMainWorld("VesktopUpdaterNative", {
getData: () => invoke<UpdateInfo>(UpdaterIpcEvents.GET_DATA),
installUpdate: () => invoke(UpdaterIpcEvents.INSTALL),
onProgress: (cb: (percent: number) => void) => {
ipcRenderer.on(UpdaterIpcEvents.DOWNLOAD_PROGRESS, (_, percent: number) => cb(percent));
},
onError: (cb: (message: string) => void) => {
ipcRenderer.on(UpdaterIpcEvents.ERROR, (_, message: string) => cb(message));
},
snoozeUpdate: () => invoke(UpdaterIpcEvents.SNOOZE_UPDATE),
ignoreUpdate: () => invoke(UpdaterIpcEvents.IGNORE_UPDATE)
});

View File

@@ -1,13 +1,12 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { filters, waitFor } from "@vencord/types/webpack"; import { filters, waitFor } from "@vencord/types/webpack";
import { RelationshipStore } from "@vencord/types/webpack/common"; import { RelationshipStore } from "@vencord/types/webpack/common";
import { VesktopLogger } from "./logger";
import { Settings } from "./settings"; import { Settings } from "./settings";
let GuildReadStateStore: any; let GuildReadStateStore: any;
@@ -27,7 +26,7 @@ export function setBadge() {
VesktopNative.app.setBadgeCount(totalCount); VesktopNative.app.setBadgeCount(totalCount);
} catch (e) { } catch (e) {
VesktopLogger.error("Failed to update badge count", e); console.error(e);
} }
} }

View File

@@ -1,67 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import type arRpcPlugin from "@vencord/types/plugins/arRPC.web";
import { Logger } from "@vencord/types/utils";
import { findLazy, findStoreLazy, onceReady } from "@vencord/types/webpack";
import { FluxDispatcher, InviteActions } from "@vencord/types/webpack/common";
import { IpcCommands } from "shared/IpcEvents";
import { onIpcCommand } from "./ipcCommands";
import { Settings } from "./settings";
const logger = new Logger("VesktopRPC", "#5865f2");
const StreamerModeStore = findStoreLazy("StreamerModeStore");
const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as typeof arRpcPlugin;
onIpcCommand(IpcCommands.RPC_ACTIVITY, async jsonData => {
if (!Settings.store.arRPC) return;
await onceReady;
const data = JSON.parse(jsonData);
if (data.socketId === "STREAMERMODE" && StreamerModeStore.autoToggle) {
FluxDispatcher.dispatch({
type: "STREAMER_MODE_UPDATE",
key: "enabled",
value: data.activity?.application_id === "STREAMERMODE"
});
return;
}
arRPC.handleEvent(new MessageEvent("message", { data: jsonData }));
});
onIpcCommand(IpcCommands.RPC_INVITE, async code => {
const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal");
if (!invite) return false;
VesktopNative.win.focus();
FluxDispatcher.dispatch({
type: "INVITE_MODAL_OPEN",
invite,
code,
context: "APP"
});
return true;
});
const { DEEP_LINK } = findLazy(m => m.DEEP_LINK?.handler);
onIpcCommand(IpcCommands.RPC_DEEP_LINK, async data => {
logger.debug("Opening deep link:", data);
try {
DEEP_LINK.handler({ args: data });
return true;
} catch (err) {
logger.error("Failed to open deep link:", err);
return false;
}
});

View File

@@ -1,9 +0,0 @@
/*
* Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2025 Vendicated and Vesktop contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { findStoreLazy } from "@vencord/types/webpack";
export const MediaEngineStore = findStoreLazy("MediaEngineStore");

View File

@@ -1,69 +1,41 @@
/* /*
* SPDX-License-Identifier: GPL-3.0
* Vesktop, a desktop app aiming to give you a snappier Discord Experience * Vesktop, a desktop app aiming to give you a snappier Discord Experience
* Copyright (c) 2023 Vendicated and Vencord contributors * Copyright (c) 2023 Vendicated and Vencord contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import "./screenSharePicker.css"; import "./screenSharePicker.css";
import { classNameFactory } from "@vencord/types/api/Styles"; import { closeModal, Modals, openModal, useAwaiter } from "@vencord/types/utils";
import { findStoreLazy, onceReady } from "@vencord/types/webpack";
import { import {
BaseText,
Button, Button,
Card, Card,
CogWheel, FluxDispatcher,
FormSwitch, Forms,
Heading, Select,
HeadingTertiary, Switch,
Margins, Text,
Paragraph, UserStore,
RestartIcon, useState
Span } from "@vencord/types/webpack/common";
} from "@vencord/types/components";
import {
closeModal,
Logger,
ModalCloseButton,
Modals,
ModalSize,
openModal,
useAwaiter,
useForceUpdater
} from "@vencord/types/utils";
import { onceReady } from "@vencord/types/webpack";
import { FluxDispatcher, Select, UserStore, useState } from "@vencord/types/webpack/common";
import { Node } from "@vencord/venmic";
import type { Dispatch, SetStateAction } from "react"; import type { Dispatch, SetStateAction } from "react";
import { MediaEngineStore } from "renderer/common";
import { addPatch } from "renderer/patches/shared"; import { addPatch } from "renderer/patches/shared";
import { State, useSettings, useVesktopState } from "renderer/settings";
import { isLinux, isWindows } from "renderer/utils"; import { isLinux, isWindows } from "renderer/utils";
import { SimpleErrorBoundary } from "./SimpleErrorBoundary"; const StreamResolutions = ["480", "720", "1080", "1440"] as const;
const StreamResolutions = ["480", "720", "1080", "1440", "2160"] as const;
const StreamFps = ["15", "30", "60"] as const; const StreamFps = ["15", "30", "60"] as const;
const cl = classNameFactory("vcd-screen-picker-"); const MediaEngineStore = findStoreLazy("MediaEngineStore");
export type StreamResolution = (typeof StreamResolutions)[number]; export type StreamResolution = (typeof StreamResolutions)[number];
export type StreamFps = (typeof StreamFps)[number]; export type StreamFps = (typeof StreamFps)[number];
type SpecialSource = "None" | "Entire System";
type AudioSource = SpecialSource | Node;
type AudioSources = SpecialSource | Node[];
interface AudioItem {
name: string;
value: AudioSource;
}
interface StreamSettings { interface StreamSettings {
resolution: StreamResolution;
fps: StreamFps;
audio: boolean; audio: boolean;
contentHint?: string; audioSource?: string;
includeSources?: AudioSources;
excludeSources?: AudioSources;
} }
export interface StreamPick extends StreamSettings { export interface StreamPick extends StreamSettings {
@@ -76,26 +48,23 @@ interface Source {
url: string; url: string;
} }
export let currentSettings: StreamSettings | null = null; let currentSettings: StreamSettings | null = null;
const logger = new Logger("VesktopScreenShare");
addPatch({ addPatch({
patches: [ patches: [
{ {
find: "this.getDefaultGoliveQuality()", find: "this.localWant=",
replacement: { replacement: {
match: /this\.getDefaultGoliveQuality\(\)/, match: /this.localWant=/,
replace: "$self.patchStreamQuality($&)" replace: "$self.patchStreamQuality(this);$&"
} }
} }
], ],
patchStreamQuality(opts: any) { patchStreamQuality(opts: any) {
const { screenshareQuality } = State.store; if (!currentSettings) return;
if (!screenshareQuality) return opts;
const framerate = Number(screenshareQuality.frameRate); const framerate = Number(currentSettings.fps);
const height = Number(screenshareQuality.resolution); const height = Number(currentSettings.resolution);
const width = Math.round(height * (16 / 9)); const width = Math.round(height * (16 / 9));
Object.assign(opts, { Object.assign(opts, {
@@ -103,34 +72,22 @@ addPatch({
bitrateMax: 8000000, bitrateMax: 8000000,
bitrateTarget: 600000 bitrateTarget: 600000
}); });
if (opts?.encode) {
Object.assign(opts.encode, {
framerate,
width,
height,
pixelCount: height * width
});
}
Object.assign(opts.capture, { Object.assign(opts.capture, {
framerate, framerate,
width, width,
height, height,
pixelCount: height * width pixelCount: height * width
}); });
return opts;
} }
}); });
if (isLinux) { if (isLinux) {
onceReady.then(() => { onceReady.then(() => {
FluxDispatcher.subscribe("STREAM_CLOSE", ({ streamKey }: { streamKey: string }) => { FluxDispatcher.subscribe("VOICE_STATE_UPDATES", e => {
const owner = streamKey.split(":").at(-1); for (const state of e.voiceStates) {
if (state.userId === UserStore.getCurrentUser().id && state.oldChannelId && !state.channelId)
if (owner !== UserStore.getCurrentUser().id) { VesktopNative.virtmic.stop();
return;
} }
VesktopNative.virtmic.stop();
}); });
}); });
} }
@@ -145,17 +102,13 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
modalProps={props} modalProps={props}
submit={async v => { submit={async v => {
didSubmit = true; didSubmit = true;
if (v.audioSource && v.audioSource !== "None") {
if (v.includeSources && v.includeSources !== "None") { if (v.audioSource === "Entire System") {
if (v.includeSources === "Entire System") { await VesktopNative.virtmic.startSystem();
await VesktopNative.virtmic.startSystem(
!v.excludeSources || isSpecialSource(v.excludeSources) ? [] : v.excludeSources
);
} else { } else {
await VesktopNative.virtmic.start(v.includeSources); await VesktopNative.virtmic.start([v.audioSource]);
} }
} }
resolve(v); resolve(v);
}} }}
close={() => { close={() => {
@@ -169,9 +122,6 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
onCloseRequest() { onCloseRequest() {
closeModal(key); closeModal(key);
reject("Aborted"); reject("Aborted");
},
onCloseCallback() {
reject("Aborted");
} }
} }
); );
@@ -180,168 +130,20 @@ export function openScreenSharePicker(screens: Source[], skipPicker: boolean) {
function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) { function ScreenPicker({ screens, chooseScreen }: { screens: Source[]; chooseScreen: (id: string) => void }) {
return ( return (
<div className={cl("screen-grid")}> <div className="vcd-screen-picker-grid">
{screens.map(({ id, name, url }) => ( {screens.map(({ id, name, url }) => (
<label key={id} className={cl("screen-label")}> <label key={id}>
<input <input type="radio" name="screen" value={id} onChange={() => chooseScreen(id)} />
type="radio"
className={cl("screen-radio")}
name="screen"
value={id}
onChange={() => chooseScreen(id)}
/>
<img src={url} alt="" /> <img src={url} alt="" />
<Paragraph className={cl("screen-name")}>{name}</Paragraph> <Text variant="text-sm/normal">{name}</Text>
</label> </label>
))} ))}
</div> </div>
); );
} }
function AudioSettingsModal({ function StreamSettings({
modalProps,
close,
setAudioSources
}: {
modalProps: any;
close: () => void;
setAudioSources: (s: AudioSources) => void;
}) {
const Settings = useSettings();
return (
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}>
<Modals.ModalHeader className={cl("header")}>
<BaseText size="lg" weight="semibold" tag="h3" style={{ flexGrow: 1 }}>
Audio Settings
</BaseText>
<ModalCloseButton onClick={close} />
</Modals.ModalHeader>
<Modals.ModalContent className={cl("modal", "venmic-settings")}>
<FormSwitch
title="Microphone Workaround"
description="Work around an issue that causes the microphone to be shared instead of the correct audio. Only enable if you're experiencing this issue."
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, workaround: v })}
value={Settings.audio?.workaround ?? false}
/>
<FormSwitch
title="Only Speakers"
description={
'When sharing entire desktop audio, only share apps that play to a speaker. You may want to disable this when using "mix bussing".'
}
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, onlySpeakers: v })}
value={Settings.audio?.onlySpeakers ?? true}
/>
<FormSwitch
title="Only Default Speakers"
description={
<>
When sharing entire desktop audio, only share apps that play to the <b>default</b> speakers.
You may want to disable this when using "mix bussing".
</>
}
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, onlyDefaultSpeakers: v })}
value={Settings.audio?.onlyDefaultSpeakers ?? true}
/>
<FormSwitch
title="Ignore Inputs"
description="Exclude nodes that are intended to capture audio."
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, ignoreInputMedia: v })}
value={Settings.audio?.ignoreInputMedia ?? true}
/>
<FormSwitch
title="Ignore Virtual"
description={
'Exclude virtual nodes, such as nodes belonging to loopbacks. This might be useful when using "mix bussing".'
}
hideBorder
onChange={v => (Settings.audio = { ...Settings.audio, ignoreVirtual: v })}
value={Settings.audio?.ignoreVirtual ?? false}
/>
<FormSwitch
title="Ignore Devices"
description="Exclude device nodes, such as nodes belonging to microphones or speakers."
hideBorder
onChange={v =>
(Settings.audio = {
...Settings.audio,
ignoreDevices: v,
deviceSelect: v ? false : Settings.audio?.deviceSelect
})
}
value={Settings.audio?.ignoreDevices ?? true}
/>
<FormSwitch
title="Granular Selection"
description="Allow to select applications more granularly."
hideBorder
onChange={value => {
Settings.audio = { ...Settings.audio, granularSelect: value };
setAudioSources("None");
}}
value={Settings.audio?.granularSelect ?? false}
/>
<FormSwitch
title="Device Selection"
description={
<>
Allow to select devices such as microphones. Requires <b>Ignore Devices</b> to be turned
off.
</>
}
hideBorder
onChange={value => {
Settings.audio = { ...Settings.audio, deviceSelect: value };
setAudioSources("None");
}}
value={Settings.audio?.deviceSelect ?? false}
disabled={Settings.audio?.ignoreDevices}
/>
</Modals.ModalContent>
<Modals.ModalFooter className={cl("footer")}>
<Button variant="secondary" onClick={close}>
Back
</Button>
</Modals.ModalFooter>
</Modals.ModalRoot>
);
}
function OptionRadio<Settings extends object, Key extends keyof Settings>(props: {
options: Array<string> | ReadonlyArray<string>;
labels?: Array<string>;
settings: Settings;
settingsKey: Key;
onChange: (option: string) => void;
}) {
const { options, settings, settingsKey, labels, onChange } = props;
return (
<div className={cl("option-radios")}>
{(options as string[]).map((option, idx) => (
<label className={cl("option-radio")} data-checked={settings[settingsKey] === option} key={option}>
<Span weight="bold">{labels?.[idx] ?? option}</Span>
<input
className={cl("option-input")}
type="radio"
name="fps"
value={option}
checked={settings[settingsKey] === option}
onChange={() => onChange(option)}
/>
</label>
))}
</div>
);
}
function StreamSettingsUi({
source, source,
settings, settings,
setSettings, setSettings,
@@ -352,9 +154,6 @@ function StreamSettingsUi({
setSettings: Dispatch<SetStateAction<StreamSettings>>; setSettings: Dispatch<SetStateAction<StreamSettings>>;
skipPicker: boolean; skipPicker: boolean;
}) { }) {
const Settings = useSettings();
const qualitySettings = State.store.screenshareQuality!;
const [thumb] = useAwaiter( const [thumb] = useAwaiter(
() => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)), () => (skipPicker ? Promise.resolve(source.url) : VesktopNative.capturer.getLargeThumbnail(source.id)),
{ {
@@ -363,88 +162,70 @@ function StreamSettingsUi({
} }
); );
const openSettings = () => {
openModal(props => (
<AudioSettingsModal
modalProps={props}
close={() => props.onClose()}
setAudioSources={sources =>
setSettings(s => ({ ...s, includeSources: sources, excludeSources: sources }))
}
/>
));
};
return ( return (
<div> <div>
<HeadingTertiary className={Margins.bottom8}>What you're streaming</HeadingTertiary> <Forms.FormTitle>What you're streaming</Forms.FormTitle>
<Card className={cl("card", "preview")}> <Card className="vcd-screen-picker-card vcd-screen-picker-preview">
<img src={thumb} alt="" className={cl(isLinux ? "preview-img-linux" : "preview-img")} /> <img src={thumb} alt="" />
<Paragraph>{source.name}</Paragraph> <Text variant="text-sm/normal">{source.name}</Text>
</Card> </Card>
<HeadingTertiary className={Margins.bottom8}>Stream Settings</HeadingTertiary> <Forms.FormTitle>Stream Settings</Forms.FormTitle>
<Card className={cl("card")}> <Card className="vcd-screen-picker-card">
<div className={cl("quality")}> <div className="vcd-screen-picker-quality">
<section className={cl("quality-section")}> <section>
<Heading tag="h5">Resolution</Heading> <Forms.FormTitle>Resolution</Forms.FormTitle>
<OptionRadio <div className="vcd-screen-picker-radios">
options={StreamResolutions} {StreamResolutions.map(res => (
settings={qualitySettings} <label className="vcd-screen-picker-radio" data-checked={settings.resolution === res}>
settingsKey="resolution" <Text variant="text-sm/bold">{res}</Text>
onChange={value => (qualitySettings.resolution = value)} <input
/> type="radio"
</section> name="resolution"
value={res}
<section className={cl("quality-section")}> checked={settings.resolution === res}
<Heading tag="h5">Frame Rate</Heading> onChange={() => setSettings(s => ({ ...s, resolution: res }))}
<OptionRadio />
options={StreamFps} </label>
settings={qualitySettings} ))}
settingsKey="frameRate" </div>
onChange={value => (qualitySettings.frameRate = value)} </section>
/>
</section> <section>
</div> <Forms.FormTitle>Frame Rate</Forms.FormTitle>
<div className={cl("quality")}> <div className="vcd-screen-picker-radios">
<section className={cl("quality-section")}> {StreamFps.map(fps => (
<Heading tag="h5">Content Type</Heading> <label className="vcd-screen-picker-radio" data-checked={settings.fps === fps}>
<div> <Text variant="text-sm/bold">{fps}</Text>
<OptionRadio <input
options={["motion", "detail"]} type="radio"
labels={["Prefer Smoothness", "Prefer Clarity"]} name="fps"
settings={settings} value={fps}
settingsKey="contentHint" checked={settings.fps === fps}
onChange={option => setSettings(s => ({ ...s, contentHint: option }))} onChange={() => setSettings(s => ({ ...s, fps }))}
/> />
</label>
<Paragraph className={Margins.top8}> ))}
Choosing "Prefer Clarity" will result in a significantly lower framerate in exchange for
a much sharper and clearer image.
</Paragraph>
</div> </div>
{isWindows && (
<FormSwitch
title="Stream With Audio"
hideBorder
value={settings.audio}
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
className={cl("audio")}
/>
)}
</section> </section>
</div> </div>
{isWindows && (
<Switch
value={settings.audio}
onChange={checked => setSettings(s => ({ ...s, audio: checked }))}
hideBorder
className="vcd-screen-picker-audio"
>
Stream With Audio
</Switch>
)}
{isLinux && ( {isLinux && (
<AudioSourcePickerLinux <AudioSourcePickerLinux
openSettings={openSettings} audioSource={settings.audioSource}
includeSources={settings.includeSources} setAudioSource={source => setSettings(s => ({ ...s, audioSource: source }))}
excludeSources={settings.excludeSources}
deviceSelect={Settings.audio?.deviceSelect}
granularSelect={Settings.audio?.granularSelect}
setIncludeSources={sources => setSettings(s => ({ ...s, includeSources: sources }))}
setExcludeSources={sources => setSettings(s => ({ ...s, excludeSources: sources }))}
/> />
)} )}
</Card> </Card>
@@ -452,249 +233,47 @@ function StreamSettingsUi({
); );
} }
function isSpecialSource(value?: AudioSource | AudioSources): value is SpecialSource {
return typeof value === "string";
}
function hasMatchingProps(value: Node, other: Node) {
return Object.keys(value).every(key => value[key] === other[key]);
}
function mapToAudioItem(node: AudioSource, granularSelect?: boolean, deviceSelect?: boolean): AudioItem[] {
if (isSpecialSource(node)) {
return [{ name: node, value: node }];
}
const rtn: AudioItem[] = [];
const mediaClass = node["media.class"];
if (mediaClass?.includes("Video") || mediaClass?.includes("Midi")) {
return rtn;
}
if (!deviceSelect && node["device.id"]) {
return rtn;
}
const name = node["application.name"];
if (name) {
rtn.push({ name: name, value: { "application.name": name } });
}
if (!granularSelect) {
return rtn;
}
const rawName = node["node.name"];
if (!name) {
rtn.push({ name: rawName, value: { "node.name": rawName } });
}
const binary = node["application.process.binary"];
if (!name && binary) {
rtn.push({ name: binary, value: { "application.process.binary": binary } });
}
const pid = node["application.process.id"];
const first = rtn[0];
const firstValues = first.value as Node;
if (pid) {
rtn.push({
name: `${first.name} (${pid})`,
value: { ...firstValues, "application.process.id": pid }
});
}
const mediaName = node["media.name"];
if (mediaName) {
rtn.push({
name: `${first.name} [${mediaName}]`,
value: { ...firstValues, "media.name": mediaName }
});
}
if (mediaClass) {
rtn.push({
name: `${first.name} [${mediaClass}]`,
value: { ...firstValues, "media.class": mediaClass }
});
}
return rtn;
}
function isItemSelected(sources?: AudioSources) {
return (value: AudioSource) => {
if (!sources) {
return false;
}
if (isSpecialSource(sources) || isSpecialSource(value)) {
return sources === value;
}
return sources.some(source => hasMatchingProps(source, value));
};
}
function updateItems(setSources: (s: AudioSources) => void, sources?: AudioSources) {
return (value: AudioSource) => {
if (isSpecialSource(value)) {
setSources(value);
return;
}
if (isSpecialSource(sources)) {
setSources([value]);
return;
}
if (isItemSelected(sources)(value)) {
setSources(sources?.filter(x => !hasMatchingProps(x, value)) ?? "None");
return;
}
setSources([...(sources || []), value]);
};
}
function AudioSourcePickerLinux({ function AudioSourcePickerLinux({
includeSources, audioSource,
excludeSources, setAudioSource
deviceSelect,
granularSelect,
openSettings,
setIncludeSources,
setExcludeSources
}: { }: {
includeSources?: AudioSources; audioSource?: string;
excludeSources?: AudioSources; setAudioSource(s: string): void;
deviceSelect?: boolean;
granularSelect?: boolean;
openSettings: () => void;
setIncludeSources: (s: AudioSources) => void;
setExcludeSources: (s: AudioSources) => void;
}) { }) {
const [audioSourcesSignal, refreshAudioSources] = useForceUpdater(true);
const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), { const [sources, _, loading] = useAwaiter(() => VesktopNative.virtmic.list(), {
fallbackValue: { ok: true, targets: [], hasPipewirePulse: true }, fallbackValue: { ok: true, targets: [] }
deps: [audioSourcesSignal]
}); });
const allSources = sources.ok ? ["None", "Entire System", ...sources.targets] : null;
const hasPipewirePulse = sources.ok ? sources.hasPipewirePulse : true;
const [ignorePulseWarning, setIgnorePulseWarning] = useState(false);
if (!sources.ok && sources.isGlibCxxOutdated) {
return (
<Paragraph>
Failed to retrieve Audio Sources because your C++ library is too old to run
<a href="https://github.com/Vencord/venmic" target="_blank" rel="noreferrer">
venmic
</a>
. See{" "}
<a
href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a"
target="_blank"
rel="noreferrer"
>
this guide
</a>{" "}
for possible solutions.
</Paragraph>
);
}
if (!hasPipewirePulse && !ignorePulseWarning) {
return (
<Paragraph>
Could not find pipewire-pulse. See{" "}
<a
href="https://gist.github.com/the-spyke/2de98b22ff4f978ebf0650c90e82027e#install"
target="_blank"
rel="noreferrer"
>
this guide
</a>{" "}
on how to switch to pipewire. <br />
You can still continue, however, please{" "}
<b>beware that you can only share audio of apps that are running under pipewire</b>.{" "}
<a onClick={() => setIgnorePulseWarning(true)}>I know what I'm doing!</a>
</Paragraph>
);
}
const specialSources: SpecialSource[] = ["None", "Entire System"] as const;
const uniqueName = (value: AudioItem, index: number, list: AudioItem[]) =>
list.findIndex(x => x.name === value.name) === index;
const allSources = sources.ok
? [...specialSources, ...sources.targets]
.map(target => mapToAudioItem(target, granularSelect, deviceSelect))
.flat()
.filter(uniqueName)
: [];
return ( return (
<> <section>
<div className={cl("audio-sources")}> <Forms.FormTitle>Audio</Forms.FormTitle>
<section> {loading && <Forms.FormTitle>Loading Audio sources...</Forms.FormTitle>}
<Heading tag="h5">{loading ? "Loading Sources..." : "Audio Sources"}</Heading> {!sources.ok &&
<SimpleErrorBoundary> (sources.isGlibcxxToOld ? (
<Select <Forms.FormText>
options={allSources.map(({ name, value }) => ({ Failed to retrieve Audio Sources because your c++ library is too old to run venmic. If you would
label: name, like to stream with Audio, see{" "}
value: value, <a href="https://gist.github.com/Vendicated/b655044ffbb16b2716095a448c6d827a" target="_blank">
default: name === "None" this guide
}))} </a>
isSelected={isItemSelected(includeSources)} </Forms.FormText>
select={updateItems(setIncludeSources, includeSources)} ) : (
serialize={String} <Forms.FormText>
popoutPosition="top" Failed to retrieve Audio Sources. If you would like to stream with Audio, make sure you're using
closeOnSelect={false} Pipewire, not Pulseaudio
/> </Forms.FormText>
</SimpleErrorBoundary> ))}
</section>
{includeSources === "Entire System" && ( {allSources && (
<section> <Select
<Heading tag="h5">Exclude Sources</Heading> options={allSources.map(s => ({ label: s, value: s, default: s === "None" }))}
<SimpleErrorBoundary> isSelected={s => s === audioSource}
<Select select={setAudioSource}
options={allSources serialize={String}
.filter(x => x.name !== "Entire System") />
.map(({ name, value }) => ({ )}
label: name, </section>
value: value,
default: name === "None"
}))}
isSelected={isItemSelected(excludeSources)}
select={updateItems(setExcludeSources, excludeSources)}
serialize={String}
popoutPosition="top"
closeOnSelect={false}
/>
</SimpleErrorBoundary>
</section>
)}
</div>
<div className={cl("settings-buttons")}>
<Button variant="secondary" onClick={refreshAudioSources} className={cl("settings-button")}>
<RestartIcon className={cl("settings-button-icon")} />
Refresh Audio Sources
</Button>
<Button variant="secondary" onClick={openSettings} className={cl("settings-button")}>
<CogWheel className={cl("settings-button-icon")} />
Open Audio Settings
</Button>
</div>
</>
); );
} }
@@ -713,28 +292,23 @@ function ModalComponent({
}) { }) {
const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0); const [selected, setSelected] = useState<string | undefined>(skipPicker ? screens[0].id : void 0);
const [settings, setSettings] = useState<StreamSettings>({ const [settings, setSettings] = useState<StreamSettings>({
contentHint: "motion", resolution: "1080",
audio: true, fps: "60",
includeSources: "None" audio: true
});
const qualitySettings = (useVesktopState().screenshareQuality ??= {
resolution: "720",
frameRate: "30"
}); });
return ( return (
<Modals.ModalRoot {...modalProps} size={ModalSize.MEDIUM}> <Modals.ModalRoot {...modalProps}>
<Modals.ModalHeader className={cl("header")}> <Modals.ModalHeader className="vcd-screen-picker-header">
<BaseText size="lg" weight="semibold" tag="h3" style={{ flexGrow: 1 }}> <Forms.FormTitle tag="h2">ScreenShare</Forms.FormTitle>
Screen Share Picker <Modals.ModalCloseButton onClick={close} />
</BaseText>
<ModalCloseButton onClick={close} />
</Modals.ModalHeader> </Modals.ModalHeader>
<Modals.ModalContent className={cl("modal")}>
<Modals.ModalContent className="vcd-screen-picker-modal">
{!selected ? ( {!selected ? (
<ScreenPicker screens={screens} chooseScreen={setSelected} /> <ScreenPicker screens={screens} chooseScreen={setSelected} />
) : ( ) : (
<StreamSettingsUi <StreamSettings
source={screens.find(s => s.id === selected)!} source={screens.find(s => s.id === selected)!}
settings={settings} settings={settings}
setSettings={setSettings} setSettings={setSettings}
@@ -742,63 +316,36 @@ function ModalComponent({
/> />
)} )}
</Modals.ModalContent> </Modals.ModalContent>
<Modals.ModalFooter className={cl("footer")}>
<Modals.ModalFooter className="vcd-screen-picker-footer">
<Button <Button
disabled={!selected} disabled={!selected}
onClick={() => { onClick={() => {
currentSettings = settings; currentSettings = settings;
try {
const frameRate = Number(qualitySettings.frameRate); // If there are 2 connections, the second one is the existing stream.
const height = Number(qualitySettings.resolution); // In that case, we patch its quality
const conn = [...MediaEngineStore.getMediaEngine().connections][1];
if (conn && conn.videoStreamParameters.length > 0) {
const height = Number(settings.resolution);
const width = Math.round(height * (16 / 9)); const width = Math.round(height * (16 / 9));
Object.assign(conn.videoStreamParameters[0], {
const conn = [...MediaEngineStore.getMediaEngine().connections].find( maxFrameRate: Number(settings.fps),
connection => connection.streamUserId === UserStore.getCurrentUser().id maxPixelCount: width * height,
); maxBitrate: 8000000,
maxResolution: {
if (conn) { type: "fixed",
conn.videoStreamParameters[0].maxFrameRate = frameRate; width,
conn.videoStreamParameters[0].maxResolution.height = height; height
conn.videoStreamParameters[0].maxResolution.width = width;
}
submit({
id: selected!,
...settings
});
setTimeout(async () => {
const conn = [...MediaEngineStore.getMediaEngine().connections].find(
connection => connection.streamUserId === UserStore.getCurrentUser().id
);
if (!conn) return;
const track = conn.input.stream.getVideoTracks()[0];
const constraints = {
...track.getConstraints(),
frameRate: { min: frameRate, ideal: frameRate },
width: { min: 640, ideal: width, max: width },
height: { min: 480, ideal: height, max: height },
advanced: [{ width: width, height: height }],
resizeMode: "none"
};
try {
await track.applyConstraints(constraints);
logger.info(
"Applied constraints successfully. New constraints:",
track.getConstraints()
);
} catch (e) {
logger.error("Failed to apply constraints.", e);
} }
}, 100); });
} catch (error) {
logger.error("Error while submitting stream.", error);
} }
submit({
id: selected!,
...settings
});
close(); close();
}} }}
> >
@@ -806,11 +353,11 @@ function ModalComponent({
</Button> </Button>
{selected && !skipPicker ? ( {selected && !skipPicker ? (
<Button variant="secondary" onClick={() => setSelected(void 0)}> <Button color={Button.Colors.TRANSPARENT} onClick={() => setSelected(void 0)}>
Back Back
</Button> </Button>
) : ( ) : (
<Button variant="secondary" onClick={close}> <Button color={Button.Colors.TRANSPARENT} onClick={close}>
Cancel Cancel
</Button> </Button>
)} )}

Some files were not shown because too many files have changed in this diff Show More