119 lines
4.2 KiB
JavaScript
119 lines
4.2 KiB
JavaScript
'use strict'
|
|
|
|
// .desktop shortcut generation: a shortcut runs the launcher with
|
|
// `--launch <id>` (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 }
|