124 lines
4.5 KiB
JavaScript
124 lines
4.5 KiB
JavaScript
'use strict'
|
|
|
|
// .desktop shortcut generation. A shortcut launches the launcher itself with
|
|
// `--launch <id>`: 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 <project>` 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 }
|