PLWL/src/shortcuts.js

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 }