diff --git a/electron/app/auto-start.ts b/electron/app/auto-start.ts index e29bcdb..6de1b3a 100644 --- a/electron/app/auto-start.ts +++ b/electron/app/auto-start.ts @@ -1,8 +1,13 @@ import { app } from 'electron'; import AutoLaunch from 'auto-launch'; +import * as fsp from 'fs/promises'; +import * as path from 'path'; import { readDesktopSettings } from '../desktop-settings'; let autoLauncher: AutoLaunch | null = null; +let autoLaunchPath = ''; + +const LINUX_AUTO_START_ARGUMENTS = ['--no-sandbox', '%U']; function resolveLaunchPath(): string { // AppImage runs from a temporary mount; APPIMAGE points to the real file path. @@ -13,15 +18,77 @@ function resolveLaunchPath(): string { return appImagePath || process.execPath; } +function escapeDesktopEntryExecArgument(argument: string): string { + const escapedArgument = argument.replace(/(["\\$`])/g, '\\$1'); + + return /[\s"]/u.test(argument) + ? `"${escapedArgument}"` + : escapedArgument; +} + +function getLinuxAutoStartDesktopEntryPath(launchPath: string): string { + return path.join(app.getPath('home'), '.config', 'autostart', `${path.basename(launchPath)}.desktop`); +} + +function buildLinuxAutoStartExecLine(launchPath: string): string { + return `Exec=${[escapeDesktopEntryExecArgument(launchPath), ...LINUX_AUTO_START_ARGUMENTS].join(' ')}`; +} + +function buildLinuxAutoStartDesktopEntry(launchPath: string): string { + const appName = path.basename(launchPath); + + return [ + '[Desktop Entry]', + 'Type=Application', + 'Version=1.0', + `Name=${appName}`, + `Comment=${appName}startup script`, + buildLinuxAutoStartExecLine(launchPath), + 'StartupNotify=false', + 'Terminal=false' + ].join('\n'); +} + +async function synchronizeLinuxAutoStartDesktopEntry(launchPath: string): Promise { + if (process.platform !== 'linux') { + return; + } + + const desktopEntryPath = getLinuxAutoStartDesktopEntryPath(launchPath); + const execLine = buildLinuxAutoStartExecLine(launchPath); + + let currentDesktopEntry = ''; + + try { + currentDesktopEntry = await fsp.readFile(desktopEntryPath, 'utf8'); + } catch { + // Create the desktop entry if auto-launch did not leave one behind. + } + + const nextDesktopEntry = currentDesktopEntry + ? /^Exec=.*$/m.test(currentDesktopEntry) + ? currentDesktopEntry.replace(/^Exec=.*$/m, execLine) + : `${currentDesktopEntry.trimEnd()}\n${execLine}\n` + : buildLinuxAutoStartDesktopEntry(launchPath); + + if (nextDesktopEntry === currentDesktopEntry) { + return; + } + + await fsp.mkdir(path.dirname(desktopEntryPath), { recursive: true }); + await fsp.writeFile(desktopEntryPath, nextDesktopEntry, 'utf8'); +} + function getAutoLauncher(): AutoLaunch | null { if (!app.isPackaged) { return null; } if (!autoLauncher) { + autoLaunchPath = resolveLaunchPath(); autoLauncher = new AutoLaunch({ name: app.getName(), - path: resolveLaunchPath() + path: autoLaunchPath }); } @@ -37,12 +104,16 @@ async function setAutoStartEnabled(enabled: boolean): Promise { const currentlyEnabled = await launcher.isEnabled(); - if (currentlyEnabled === enabled) { + if (!enabled && currentlyEnabled === enabled) { return; } if (enabled) { - await launcher.enable(); + if (!currentlyEnabled) { + await launcher.enable(); + } + + await synchronizeLinuxAutoStartDesktopEntry(autoLaunchPath); return; }