libvesktop: native dbus module for autostart & app badge (#1180)

Both setAppBadge and autoStart at system boot now use dbus calls. This means that autoStart will work in Flatpak
This commit is contained in:
V
2025-10-16 10:52:52 +02:00
committed by GitHub
parent 8cc34e217c
commit 5734a1d33c
28 changed files with 1338 additions and 167 deletions

View File

@@ -17,7 +17,7 @@ Vesktop is a custom Discord desktop app
## Installing
Visit https://vesktop.vencord.dev/install
Visit https://vesktop.dev/install
## Building from Source
@@ -47,3 +47,14 @@ pnpm package --linux pacman
# Or package to a directory only
pnpm package:dir
```
## Building LibVesktop from Source
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.
If you want to build it from source:
1. Install build dependencies:
- Debian/Ubuntu: `apt install build-essential python3 curl pkg-config libglib2.0-dev`
- 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

View File

@@ -11,6 +11,7 @@
"scripts": {
"build": "tsx scripts/build/build.mts",
"build:dev": "pnpm build --dev",
"buildLibVesktop": "pnpm -C packages/libvesktop install && pnpm -C packages/libvesktop run build",
"package": "pnpm build && electron-builder",
"package:dir": "pnpm build && electron-builder --dir",
"lint": "eslint",
@@ -49,6 +50,7 @@
"eslint-plugin-simple-header": "^1.2.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.2.0",
"libvesktop": "link:packages/libvesktop",
"prettier": "^3.6.2",
"source-map-support": "^0.5.21",
"tsx": "^4.20.6",

1
packages/libvesktop/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build

View File

@@ -0,0 +1,19 @@
# 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

@@ -0,0 +1,19 @@
{
"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"],
}
]
}

17
packages/libvesktop/build.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/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
"

3
packages/libvesktop/index.d.ts vendored Normal file
View File

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

View File

@@ -0,0 +1,14 @@
{
"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"
}
}

730
packages/libvesktop/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,730 @@
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: {}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,269 @@
#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

@@ -0,0 +1,22 @@
/**
* @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);
});

3
pnpm-lock.yaml generated
View File

@@ -71,6 +71,9 @@ importers:
eslint-plugin-unused-imports:
specifier: ^4.2.0
version: 4.2.0(@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@9.37.0)(typescript@5.9.3))(eslint@9.37.0)(typescript@5.9.3))(eslint@9.37.0)
libvesktop:
specifier: link:packages/libvesktop
version: link:packages/libvesktop
prettier:
specifier: ^3.6.2
version: 3.6.2

View File

@@ -25,6 +25,9 @@ const NodeCommonOpts: BuildOptions = {
platform: "node",
external: ["electron"],
target: ["esnext"],
loader: {
".node": "file"
},
define: {
IS_DEV: JSON.stringify(isDev)
}
@@ -50,8 +53,29 @@ async function copyVenmic() {
]).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([
copyVenmic(),
copyLibVesktop(),
createContext({
...NodeCommonOpts,
entryPoints: ["src/main/index.ts"],

View File

@@ -8,7 +8,9 @@ import { app, NativeImage, nativeImage } from "electron";
import { join } from "path";
import { BADGE_DIR } from "shared/paths";
import { updateUnityLauncherCount } from "./dbus";
import { AppEvents } from "./events";
import { mainWin } from "./mainWindow";
const imgCache = new Map<number, NativeImage>();
function loadBadge(index: number) {
@@ -33,7 +35,7 @@ export function setBadgeCount(count: number) {
switch (process.platform) {
case "linux":
if (count === -1) count = 0;
app.setBadgeCount(count);
updateUnityLauncherCount(count);
break;
case "darwin":
if (count === 0) {
@@ -48,8 +50,6 @@ export function setBadgeCount(count: number) {
lastIndex = index;
// circular import shenanigans
const { mainWin } = require("./mainWindow") as typeof import("./mainWindow");
mainWin.setOverlayIcon(index === null ? null : loadBadge(index), description);
break;
}

View File

@@ -5,30 +5,32 @@
*/
import { app } from "electron";
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "fs";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
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 {
isEnabled(): boolean;
enable(): void;
disable(): void;
}
function makeAutoStartLinux(): AutoStart {
function getEscapedCommandLine() {
const args = process.argv.map(escapeDesktopFileArgument);
if (Settings.store.autoStartMinimized) args.push("--start-minimized");
return args;
}
function makeAutoStartLinuxDesktop(): AutoStart {
const configDir = process.env.XDG_CONFIG_HOME || join(process.env.HOME!, ".config");
const dir = join(configDir, "autostart");
const file = join(dir, "vesktop.desktop");
// IM STUPID
const legacyName = join(dir, "vencord.desktop");
if (existsSync(legacyName)) renameSync(legacyName, file);
// "Quoting must be done by enclosing the argument between double quotes and escaping the double quote character,
// backtick character ("`"), dollar sign ("$") and backslash character ("\") by preceding it with an additional backslash character"
// https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables
const commandLine = process.argv.map(arg => '"' + arg.replace(/["$`\\]/g, "\\$&") + '"').join(" ");
return {
isEnabled: () => existsSync(file),
enable() {
@@ -37,7 +39,7 @@ function makeAutoStartLinux(): AutoStart {
Type=Application
Name=Vesktop
Comment=Vesktop autostart script
Exec=${commandLine}
Exec=${getEscapedCommandLine().join(" ")}
StartupNotify=false
Terminal=false
Icon=vesktop
@@ -50,10 +52,49 @@ function makeAutoStartLinux(): AutoStart {
};
}
function makeAutoStartLinuxPortal() {
return {
isEnabled: () => State.store.linuxAutoStartEnabled === true,
enable() {
const success = requestBackground(true, getEscapedCommandLine());
if (success) {
State.store.linuxAutoStartEnabled = true;
}
return success;
},
disable() {
const success = requestBackground(false, []);
if (success) {
State.store.linuxAutoStartEnabled = false;
}
return success;
}
};
}
const autoStartWindowsMac: AutoStart = {
isEnabled: () => app.getLoginItemSettings().openAtLogin,
enable: () => app.setLoginItemSettings({ openAtLogin: true }),
enable: () =>
app.setLoginItemSettings({
openAtLogin: true,
args: Settings.store.autoStartMinimized ? ["--start-minimized"] : []
}),
disable: () => app.setLoginItemSettings({ openAtLogin: false })
};
export const autoStart = process.platform === "linux" ? makeAutoStartLinux() : autoStartWindowsMac;
// The portal call uses the app id by default, which is org.chromium.Chromium, even in packaged Vesktop.
// 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

@@ -20,7 +20,7 @@ export const DATA_DIR =
mkdirSync(DATA_DIR, { recursive: true });
const SESSION_DATA_DIR = join(DATA_DIR, "sessionData");
export const SESSION_DATA_DIR = join(DATA_DIR, "sessionData");
app.setPath("sessionData", SESSION_DATA_DIR);
export const VENCORD_SETTINGS_DIR = join(DATA_DIR, "settings");
@@ -28,12 +28,6 @@ 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_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")).State.store.vencordDir ||
join(SESSION_DATA_DIR, "vencordFiles");
export const USER_AGENT = `Vesktop/${app.getVersion()} (https://github.com/Vencord/Vesktop)`;
// dimensions shamelessly stolen from Discord Desktop :3
@@ -57,3 +51,5 @@ export const enum MessageBoxChoice {
Default,
Cancel
}
export const IS_FLATPAK = process.env.FLATPAK_ID !== undefined;

40
src/main/dbus.ts Normal file
View File

@@ -0,0 +1,40 @@
/*
* 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

@@ -28,13 +28,14 @@ import { debounce } from "shared/utils/debounce";
import { IpcEvents } from "../shared/IpcEvents";
import { setBadgeCount } from "./appBadge";
import { autoStart } from "./autoStart";
import { VENCORD_FILES_DIR, VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants";
import { VENCORD_QUICKCSS_FILE, VENCORD_THEMES_DIR } from "./constants";
import { mainWin } from "./mainWindow";
import { Settings, State } from "./settings";
import { handle, handleSync } from "./utils/ipcWrappers";
import { PopoutWindows } from "./utils/popout";
import { isDeckGameMode, showGamePage } from "./utils/steamOS";
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_RENDERER_SCRIPT, () =>

View File

@@ -22,7 +22,7 @@ import type { SettingsStore } from "shared/utils/SettingsStore";
import { createAboutWindow } from "./about";
import { initArRPC } from "./arrpc";
import { BrowserUserAgent, DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH, VENCORD_FILES_DIR } from "./constants";
import { BrowserUserAgent, DEFAULT_HEIGHT, DEFAULT_WIDTH, MIN_HEIGHT, MIN_WIDTH } from "./constants";
import { AppEvents } from "./events";
import { darwinURL } from "./index";
import { sendRendererCommand } from "./ipcCommands";
@@ -33,6 +33,7 @@ import { clearData } from "./utils/clearData";
import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally";
import { applyDeckKeyboardFix, askToApplySteamLayout, isDeckGameMode } from "./utils/steamOS";
import { downloadVencordFiles, ensureVencordFiles } from "./utils/vencordLoader";
import { VENCORD_FILES_DIR } from "./vencordFilesDir";
let isQuitting = false;

View File

@@ -0,0 +1,56 @@
/*
* 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

@@ -6,9 +6,10 @@
import { mkdirSync } from "fs";
import { access, constants as FsConstants, writeFile } from "fs/promises";
import { VENCORD_FILES_DIR } from "main/vencordFilesDir";
import { join } from "path";
import { USER_AGENT, VENCORD_FILES_DIR } from "../constants";
import { USER_AGENT } from "../constants";
import { downloadFile, fetchie } from "./http";
const API_BASE = "https://api.github.com";

View File

@@ -0,0 +1,13 @@
/*
* 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

@@ -9,18 +9,28 @@ import { useState } from "@vencord/types/webpack/common";
import { SettingsComponent } from "./Settings";
import { VesktopSettingsSwitch } from "./VesktopSettingsSwitch";
export const AutoStartToggle: SettingsComponent = () => {
export const AutoStartToggle: SettingsComponent = ({ settings }) => {
const [autoStartEnabled, setAutoStartEnabled] = useState(VesktopNative.autostart.isEnabled());
return (
<VesktopSettingsSwitch
title="Start With System"
description="Automatically start Vesktop on computer start-up"
value={autoStartEnabled}
onChange={async v => {
await VesktopNative.autostart[v ? "enable" : "disable"]();
setAutoStartEnabled(v);
}}
/>
<>
<VesktopSettingsSwitch
title="Start With System"
description="Automatically start Vesktop on computer start-up"
value={autoStartEnabled}
onChange={async v => {
await VesktopNative.autostart[v ? "enable" : "disable"]();
setAutoStartEnabled(v);
}}
/>
<VesktopSettingsSwitch
title="Auto Start Minimized"
description={"Start Vesktop minimized when starting with system"}
value={settings.autoStartMinimized ?? false}
onChange={v => (settings.autoStartMinimized = v)}
disabled={!autoStartEnabled}
/>
</>
);
};

View File

@@ -11,6 +11,7 @@ export interface Settings {
transparencyOption?: "none" | "mica" | "tabbed" | "acrylic";
tray?: boolean;
minimizeToTray?: boolean;
autoStartMinimized?: boolean;
openLinksWithElectron?: boolean;
staticTitle?: boolean;
enableMenu?: boolean;
@@ -54,6 +55,7 @@ export interface State {
firstLaunch?: boolean;
steamOSLayoutVersion?: number;
linuxAutoStartEnabled?: boolean;
vencordDir?: string;
}

View File

@@ -28,10 +28,10 @@
<h2>Links</h2>
<ul>
<li>
<a href="https://vesktop.vencord.dev/wiki" target="_blank">Vesktop Wiki</a>
<a href="https://vesktop.dev" target="_blank">Vesktop Website</a>
</li>
<li>
<a href="https://vencord.dev" target="_blank">Vencord Website</a>
<a href="https://vesktop.dev/wiki" target="_blank">Vesktop Wiki</a>
</li>
<li>
<a href="https://github.com/Vencord/Vesktop" target="_blank">Source Code</a>
@@ -76,9 +76,8 @@
</li>
<li>
And many
<a href="https://github.com/Vencord/Vesktop/blob/main/pnpm-lock.yaml" target="_blank"
>more open source libraries</a
>
<a href="https://github.com/Vencord/Vesktop/blob/main/pnpm-lock.yaml" target="_blank">more open source
libraries</a>
</li>
</ul>
</section>
@@ -104,4 +103,4 @@
}
walk(document.body);
</script>
</script>

View File

@@ -1,123 +0,0 @@
<head>
<link rel="stylesheet" href="./style.css" type="text/css" />
<style>
.wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
min-height: 100%;
padding: 1em;
}
h1 {
text-align: center;
}
.buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5em;
margin-top: 0.25em;
}
button {
cursor: pointer;
padding: 0.5em;
color: var(--fg);
border: none;
border-radius: 3px;
font-weight: bold;
transition: filter 0.2 ease-in-out;
}
button:hover,
button:active {
filter: brightness(0.9);
}
.green {
background-color: #248046;
}
.red {
background-color: #ed4245;
}
</style>
</head>
<body>
<div class="wrapper">
<section>
<h1>Update Available</h1>
<p>There's a new update for Vesktop! Update now to get new fixes and features!</p>
<p>
Current: <span id="current"></span>
<br />
Latest: <span id="latest"></span>
</p>
<h2>Changelog</h2>
<p id="changelog">Loading...</p>
</section>
<section>
<label id="disable-remind">
<input type="checkbox" />
<span>Do not remind again for </span>
</label>
<div class="buttons">
<button name="download" class="green">Download Update</button>
<button name="close" class="red">Close</button>
</div>
</section>
</div>
</body>
<script type="module">
const data = await Updater.getData();
document.getElementById("current").textContent = data.currentVersion;
document.getElementById("latest").textContent = data.latestVersion;
document.querySelector("#disable-remind > span").textContent += data.latestVersion;
function checkDisableRemind() {
const checkbox = document.querySelector("#disable-remind > input");
if (checkbox.checked) {
Updater.ignore();
}
}
const onClicks = {
download() {
checkDisableRemind();
Updater.download();
},
close() {
checkDisableRemind();
Updater.close();
}
};
for (const name in onClicks) {
document.querySelectorAll(`button[name="${name}"]`).forEach(button => {
button.addEventListener("click", onClicks[name]);
});
}
</script>
<script type="module">
import { micromark } from "https://esm.sh/micromark@3?bundle";
import { gfm, gfmHtml } from "https://esm.sh/micromark-extension-gfm@2?bundle";
const changelog = (await Updater.getData()).release.body;
if (changelog)
document.getElementById("changelog").innerHTML = micromark(changelog, {
extensions: [gfm()],
htmlExtensions: [gfmHtml()]
})
.replace(/h1>/g, "h3>")
.replace(/<a /g, '<a target="_blank" ');
</script>