'use strict' // .desktop shortcut generation. A shortcut launches the launcher itself with // `--launch `: the main process either starts the game in the running // instance or boots headless (no window, tray only), starts the game and lets // playtime tracking continue normally. const fs = require('fs') const path = require('path') const { DIRS, resolvePortablePath, launcherIconDiskPath } = require('./paths') const DATA_DIR = process.env.PLWL_BUILD_DIR ? path.join(process.env.PLWL_BUILD_DIR, 'files', 'data') : DIRS.DATA // Where user-visible shortcuts live (XDG). Override with PLWL_SHORTCUTS_DIR // so tests (and portable exotic setups) can redirect it. function desktopDirs() { const home = process.env.HOME || '/' const dataHome = process.env.XDG_DATA_HOME || path.join(home, '.local', 'share') return { menu: process.env.PLWL_SHORTCUTS_DIR || path.join(dataHome, 'applications'), desktop: process.env.PLWL_SHORTCUTS_DIR || path.join(home, 'Desktop') } } // Launcher executable as the shortcut will see it, as an argv list: the // packaged olcxja.plwl/olcxja.plwl binary, or `electron ` in dev. function launcherArgv() { if (process.env.PLWL_LAUNCHER_CMD) return process.env.PLWL_LAUNCHER_CMD.split(' ').filter(Boolean) const m = __dirname.match(/^(.*)[/\\]app\.asar([/\\]|$)/) if (m) { const root = path.resolve(m[1], '..', '..') const bin = path.join(root, 'olcxja.plwl', 'olcxja.plwl') if (fs.existsSync(bin)) return [bin] } // dev fallback: run through electron from the project dir return ['electron', path.resolve(__dirname, '..')] } // Desktop Entry Exec field escaping (per the spec): % must be doubled, // quotes and backslashes escaped, and tokens containing spaces wrapped in // double quotes. function execToken(token) { const escaped = token.replace(/%/g, '%%').replace(/\\/g, '\\\\').replace(/"/g, '\\"') return /[ $"'\\]/.test(token) ? `"${escaped}"` : escaped } function execLine(extraArgs) { return [...launcherArgv(), ...extraArgs.split(' ').filter(Boolean).map(execToken)].map(execToken).join(' ') } function sanitizeName(name) { return String(name || 'App').replace(/[\r\n]/g, ' ').trim() || 'App' } // Builds the .desktop file content for launching app `id`. function buildDesktopEntry(game) { const name = sanitizeName(game.name) const iconDir = path.join(DATA_DIR, 'icons', game.id) let icon = '' try { const found = fs.readdirSync(iconDir).filter(f => f.startsWith('icon.')).sort().pop() if (found) icon = path.join(iconDir, found) } catch {} if (!icon) { // fall back to the launcher icon so the shortcut is never naked (the // on-disk copy: the desktop environment cannot read inside app.asar) icon = launcherIconDiskPath() || '' } const lines = [ '[Desktop Entry]', 'Type=Application', `Name=${name}`, 'Comment=Launch via PLWL', `Exec=${execLine(`--launch ${game.id}`)}`, 'Terminal=false', 'Categories=Game;' ] if (icon) lines.push(`Icon=${icon}`) return lines.join('\n') + '\n' } // Where a given shortcut would be written (used by tests and by the UI hint). function shortcutPaths(gameId) { const dirs = desktopDirs() return { menu: path.join(dirs.menu, `plwl-${gameId}.desktop`), desktop: path.join(dirs.desktop, `plwl-${gameId}.desktop`) } } // Writes the shortcut. Returns the paths it created. function createShortcut(game) { const content = buildDesktopEntry(game) const dirs = desktopDirs() const written = [] for (const [key, dir] of [['menu', dirs.menu], ['desktop', dirs.desktop]]) { try { fs.mkdirSync(dir, { recursive: true }) const file = path.join(dir, `plwl-${game.id}.desktop`) fs.writeFileSync(file, content) if (key === 'menu') { // update desktop databases best-effort so the entry shows up at once try { require('child_process').execFileSync('update-desktop-database', [dir], { timeout: 10000 }) } catch {} } written.push(file) } catch (err) { // a read-only Desktop must not fail the whole operation if (key === 'desktop') continue throw err } } return { ok: true, paths: written } } // Removes shortcuts for a game (also called when the game is removed). function removeShortcut(gameId) { const dirs = desktopDirs() for (const file of [path.join(dirs.menu, `plwl-${gameId}.desktop`), path.join(dirs.desktop, `plwl-${gameId}.desktop`)]) { try { fs.rmSync(file, { force: true }) } catch {} } } module.exports = { buildDesktopEntry, createShortcut, removeShortcut, shortcutPaths, launcherArgv }