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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
40
src/main/dbus.ts
Normal 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;
|
||||
}
|
||||
@@ -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, () =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
56
src/main/utils/desktopFileEscape.ts
Normal file
56
src/main/utils/desktopFileEscape.ts
Normal 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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
13
src/main/vencordFilesDir.ts
Normal file
13
src/main/vencordFilesDir.ts
Normal 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");
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
2
src/shared/settings.d.ts
vendored
2
src/shared/settings.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user