'use strict' // .desktop shortcut generation: a shortcut runs the launcher with // `--launch ` (headless boot when nothing is running yet). 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 // XDG shortcuts dir; PLWL_SHORTCUTS_DIR overrides it (tests). 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') } } // The launcher executable as an argv list (packaged binary or electron 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 escaping per the spec (% doubled, quotes, spaces). function execToken(token) { const escaped = token.replace(/%/g, '%%').replace(/\\/g, '\\\\').replace(/"/g, '\\"') return /[ $"'\\]/.test(token) ? `"${escaped}"` : escaped } // %f appended after escaping makes the shortcut a file handler. function execLine(extraArgs) { const tokens = [...launcherArgv(), ...String(extraArgs).split(' ').filter(Boolean)] return [...tokens.map(execToken), '%f'].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) { // on-disk launcher icon fallback (Icon= cannot read the asar) icon = launcherIconDiskPath() || '' } const lines = [ '[Desktop Entry]', 'Type=Application', `Name=${name}`, 'Comment=Launch via PLWL', `Exec=${execLine(`--launch ${game.id} --file`)}`, '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 }