'use strict' const { app } = require('electron') const { ensureDirs, DIRS } = require('./paths') const i18n = require('./i18n') const wines = require('./wines') const { registerIpc, sendToWindow } = require('./ipc') const { createMainWindow } = require('./window') const { setAppIcon } = require('./appicon') const { parseArgv } = require('./argv') const openwith = require('./openwith') const lansync = require('./lansync') const path = require('path') const fs = require('fs') app.setName('PLWL') // The launcher keeps running with only a tray icon, so closing the window // must not quit the app (playtime keeps counting while a game runs). app.on('window-all-closed', e => { // no-op on purpose: tray Quit / menu Quit is the way out }) let win = null let tray = null let quitting = false let headless = false // started via --launch: no window until the user asks let autoLaunchId = null // game id from --launch let autoOpenPath = null // file path from --open ("Open with PLWL") let autoQuitWhenDone = false // headless boot: quit after that game exits const getWindow = () => win // --- single instance --------------------------------------------------------- // Diagnostics: the shortcut handoff path is invisible otherwise (a failing // handoff just "does nothing" for the user). Everything lands in a small // trace file next to the launcher data so problems are reproducible. function trace(msg) { try { const file = path.join(DIRS.DATA, 'instance-trace.log') // keep the trace tiny: drop it once it grows past ~64 KiB try { if (fs.existsSync(file) && fs.statSync(file).size > 64 * 1024) fs.rmSync(file, { force: true }) } catch {} fs.appendFileSync(file, `${new Date().toISOString()} ${msg}\n`) } catch {} } // A second launcher invocation (e.g. a desktop shortcut with --launch ) // hands its arguments to the running instance and exits immediately. const gotLock = app.requestSingleInstanceLock() trace(`boot argv=${process.argv.slice(1).join(' ')} gotLock=${gotLock}`) if (!gotLock) { app.quit() } else { app.on('second-instance', (e, argv) => { trace(`second-instance argv=${argv.join(' ')}`) const { launchId, openPath } = parseArgv(argv) lastHandoffAt = Date.now() if (openPath) openWith(openPath) else if (launchId) launchGame(launchId) else showMainWindow() }) } // set on every second-instance handoff: the auto-quit check skips itself for // a moment afterwards, so "one game exits" racing "a shortcut just started // another game" can never kill the launcher mid-handoff let lastHandoffAt = 0 // --- "Open with PLWL" (Exec %f from the file manager) -------------------------- // Hands the file to the window (renderer shows the wine/prefix picker popup); // headless boots open the window for it - a file cannot be launched without // the user choosing how. async function openWith(filePath) { trace(`openWith ${filePath} serviceReady=${!!gamesService}`) showMainWindow() if (gamesService) gamesService.openExternally(filePath) else openwith.setPending(filePath) } const auto = parseArgv(process.argv) autoLaunchId = auto.launchId autoOpenPath = auto.openPath headless = !!autoLaunchId autoQuitWhenDone = headless // booting just to run one game -> exit when it ends // --- tray --------------------------------------------------------------------- // Lives as long as the app does: shows "active in background" even when the // window is closed, and offers running games + Quit. function trayIconPath() { return require('./paths').launcherIconPath() || undefined } function createTray() { const { Tray, Menu } = require('electron') const icon = trayIconPath() try { tray = new Tray(icon) } catch (err) { // never fatal (the window keeps working), but say why the icon is missing // instead of failing silently - a missing tray looks like a hung app when // the launcher runs in the background console.error(`[tray] could not create the tray icon (${icon || 'no icon file found'}): ${(err && err.message) || err}`) tray = null return } tray.setToolTip('PLWL') rebuildTrayMenu() } function rebuildTrayMenu() { if (!tray) return const { Menu } = require('electron') const lang = loadLangSafe() const gamesSvc = gamesService const running = gamesSvc ? gamesSvc.listRunning() : [] const items = [] items.push({ label: i18n.translate(lang, 'tray.open'), click: () => showMainWindow() }) if (running.length) { items.push({ type: 'separator' }) items.push({ label: i18n.translate(lang, 'tray.running'), enabled: false }) for (const id of running) { const game = require('./library').getGame(id) if (!game) continue items.push({ label: `• ${game.name}`, click: () => { // click toggles: stop the running game gamesSvc.kill(id) } }) } } items.push({ type: 'separator' }) items.push({ label: i18n.translate(lang, 'tray.quit'), click: () => { quitting = true app.quit() } }) tray.setContextMenu(Menu.buildFromTemplate(items)) const tip = running.length ? `PLWL - ${i18n.translate(lang, 'tray.tooltipRunning', running.length)}` : 'PLWL' tray.setToolTip(tip) } function loadLangSafe() { try { return require('./library').loadSettings().language || 'en-us' } catch { return 'en-us' } } function showMainWindow() { // the user is present now - headless auto-quit must never fire again headless = false autoQuitWhenDone = false if (gamesService) gamesService.setOnGameExit(null) if (win && !win.isDestroyed()) { win.show() win.focus() return } win = createMainWindow() wireWindow(win) } function wireWindow(w) { // closing the window hides it to the tray (playtime keeps counting in the // background) unless the user disabled that in settings - then it quits w.on('close', e => { if (quitting) return let closeToTray = true try { closeToTray = require('./library').loadSettings().closeToTray !== false } catch {} if (closeToTray) { e.preventDefault() w.hide() } else { quitting = true app.quit() } }) w.on('closed', () => { win = null }) } // --- game auto-launch (shortcuts) ----------------------------------------------- let gamesService = null async function launchGame(id) { trace(`launchGame ${id} serviceReady=${!!gamesService}`) if (!gamesService) return const res = await gamesService.start(getWindow(), id) trace(`launchGame ${id} result=${JSON.stringify(res).slice(0, 200)}`) if (res.ok) { rebuildTrayMenu() // headless boot: when the auto-started game finishes and nothing else is // running, quit the launcher if (autoQuitWhenDone) { gamesService.setOnGameExit(() => { // fires for EVERY game exit; quit only when the last one just ended, // the user never opened the window and no shortcut handed a new game // over in the last few seconds (start/exit race guard) if (Date.now() - lastHandoffAt < 5000) return if (!gamesService.listRunning().length) { quitting = true app.quit() } }) } } else if (headless && autoQuitWhenDone) { // nothing to run (bad shortcut target) -> do not linger in the tray quitting = true app.quit() } } app.whenReady().then(async () => { if (process.platform === 'linux') { try { setAppIcon(app) } catch {} } ensureDirs() await i18n.init() await wines.refreshWines() const { games } = registerIpc({ getWindow, onGamesChanged: rebuildTrayMenu }) gamesService = games // the tray comes first: nothing (least of all the network) may keep it from // coming up - a launcher without a tray icon looks simply broken createTray() // LAN sync runs whenever the launcher runs - that is what makes this // device show up on the other machines' client lists const lanHooks = { onDevices: devices => sendToWindow(win, 'lansync:devices', devices), onApplied: info => sendToWindow(win, 'lansync:applied', info), onReceive: info => sendToWindow(win, 'lansync:receiving', info), isBusy: () => !!(gamesService && gamesService.listRunning().length) } try { Promise.resolve(lansync.start(lanHooks)).catch(err => { console.error(`[lansync] start failed: ${(err && err.message) || err}`) }) } catch (err) { console.error(`[lansync] start failed: ${(err && err.message) || err}`) } if (headless) { // shortcut boot: run the game without opening the launcher window launchGame(autoLaunchId) } else { win = createMainWindow() wireWindow(win) // file open handed over while the service was still booting (or queued // by a second instance racing the first boot) if (autoOpenPath || openwith.hasPending()) { openWith(autoOpenPath || openwith.getPending()) } } }) app.on('before-quit', () => { quitting = true try { lansync.stop() } catch {} })