diff --git a/README.md b/README.md
index 0a88a00..005b7fe 100644
--- a/README.md
+++ b/README.md
@@ -43,10 +43,6 @@ cd PLWL
Besides copy/move/leave, an app can be imported **from a wine prefix**: pick the prefix, then the app folder inside its `drive_c`. Nothing is copied — paths are stored relative to the prefix, so wine sees the app as if it were normally installed on `C:\` (registry entries pointing to `C:\...` keep working). Both layouts work: plain wine prefixes (`drive_c` at the top) and Proton-managed ones (`drive_c` under `pfx/`). The app stays tied to that prefix; removing the launcher entry never deletes files from the prefix.
-## "Open with PLWL" in the file manager
-
-The desktop entry (`MimeType` + `Exec %f`) makes PLWL show up in Dolphin/Nautilus under **Open With** for Windows executables (`.exe`, `.msi`, `.bat`, `.cmd`, `.lnk`). Opening a file that way shows a small popup in the launcher where you pick the **Wine/Proton version** and the **prefix** for this run — both default to the launcher's defaults, nothing is imported into the library. A second launcher instance hands the path to the running one and exits (same single-instance mechanism the shortcuts use).
-
## Wine & Proton support
- **System Wine** — uses the `wine` binary from PATH.
@@ -56,5 +52,3 @@ The desktop entry (`MimeType` + `Exec %f`) makes PLWL show up in Dolphin/Nautilu
## Translations
All UI text comes from JSON files in `files/translations/`. The `data` object carries the language names; `texts` maps keys to strings, with `{0}`, `{1}` ... placeholders filled at runtime.
-
-The translations bundled with a release are launcher-owned: on every start they are refreshed into `files/translations/` whenever their content changed, so launcher updates deliver new/changed keys to existing installs. Extra language files the bundle does not ship (user-added) are never touched.
diff --git a/files/translations/en-us.json b/files/translations/en-us.json
index 8c455f7..c06afd5 100644
--- a/files/translations/en-us.json
+++ b/files/translations/en-us.json
@@ -144,16 +144,6 @@
"settings.saved": "Settings saved",
"settings.openFiles": "Open files folder",
"settings.closeToTray": "Close window to tray (keep tracking playtime in background)",
- "openwith.title": "Open with Wine",
- "openwith.file": "File",
- "openwith.fileMissing": "This file no longer exists (or is not a Windows executable).",
- "openwith.wine": "Wine / Proton version",
- "openwith.wineDefault": "Default ({0})",
- "openwith.prefix": "Wine prefix",
- "openwith.prefixDefault": "Default prefix ({0})",
- "openwith.run": "Run",
- "openwith.startFailed": "Could not start {0}: {1}",
- "openwith.notWindows": "This is not a Windows executable (exe/msi/bat/cmd/lnk).",
"dialog.ok": "OK",
"dialog.cancel": "Cancel",
"dialog.close": "Close",
diff --git a/files/translations/pl.json b/files/translations/pl.json
index 7536f51..7c8568e 100644
--- a/files/translations/pl.json
+++ b/files/translations/pl.json
@@ -144,16 +144,6 @@
"settings.saved": "Ustawienia zapisane",
"settings.openFiles": "Otwórz folder files",
"settings.closeToTray": "Zamykaj okno do zasobnika systemowego (licz czas gry w tle)",
- "openwith.title": "Otwórz przez Wine",
- "openwith.file": "Plik",
- "openwith.fileMissing": "Ten plik już nie istnieje (albo nie jest plikiem wykonywalnym Windows).",
- "openwith.wine": "Wersja Wine / Proton",
- "openwith.wineDefault": "Domyślna ({0})",
- "openwith.prefix": "Prefiks Wine",
- "openwith.prefixDefault": "Prefiks domyślny ({0})",
- "openwith.run": "Uruchom",
- "openwith.startFailed": "Nie udało się uruchomić {0}: {1}",
- "openwith.notWindows": "To nie jest plik wykonywalny Windows (exe/msi/bat/cmd/lnk).",
"dialog.ok": "OK",
"dialog.cancel": "Anuluj",
"dialog.close": "Zamknij",
diff --git a/package.json b/package.json
index e071dd5..7a04c09 100644
--- a/package.json
+++ b/package.json
@@ -1,11 +1,13 @@
{
"name": "PLWL",
"productName": "PLWL",
- "version": "0.1.2",
+ "version": "0.1.1",
"description": "Portable Linux Wine Launcher",
"main": "src/main.js",
"scripts": {
- "start": "electron ."
+ "start": "electron .",
+ "test": "node test/import.test.js && node test/prefix-import.test.js && node test/icons.test.js && node test/shortcuts.test.js && node test/wines-portable.test.js",
+ "smoke": "node test/import.test.js && node test/prefix-import.test.js && node test/icons.test.js && node test/shortcuts.test.js && node test/wines-portable.test.js"
},
"devDependencies": {
"@electron/asar": "^3.4.1",
diff --git a/renderer/app.js b/renderer/app.js
index 009e501..f9ccae2 100644
--- a/renderer/app.js
+++ b/renderer/app.js
@@ -597,114 +597,6 @@ async function doImport(srcDir, mode) {
}
}
-// --- "Open with PLWL" (file manager -> Open With -> PLWL) -----------------------
-
-const openwithTexts = {}
-
-function ot(key, ...args) {
- let text = openwithTexts[key] ?? t(key)
- args.forEach((v, i) => {
- text = String(text).split(`{${i}}`).join(String(v))
- })
- return text
-}
-
-async function loadOpenwithTexts() {
- try {
- Object.assign(openwithTexts, await window.plwl.openwithTexts(state.lang))
- } catch {}
-}
-
-function isWindowsExecutable(p) {
- return /\.(exe|msi|bat|cmd|lnk)$/i.test(p || '')
-}
-
-// The popup: wine version + prefix selects defaulting to the LAUNCHER's
-// defaults (settings.defaultWine / prefixes.default), like the per-app
-// config's "Default (...)" options - just without importing anything.
-async function showOpenWithModal(openPath) {
- // always fresh: wines/prefixes/default can change while the launcher sits
- // in the tray
- try {
- const [wines, prefixes, settings] = await Promise.all([
- window.plwl.winesList(),
- window.plwl.prefixesList(),
- window.plwl.settingsGet()
- ])
- state.wines = wines
- state.prefixes = prefixes
- state.settings = settings
- } catch {}
- const modal = showModal(`
-
${ot('openwith.title')}
-
${ot('openwith.file')}:
-
${ot('openwith.fileMissing')}
-
-
-
-
-
-
-
-
- `)
- modal.querySelector('#ow-file').textContent = openPath
- modal.querySelector('#ow-file').title = openPath
- const missing = !window.plwl.fileExists(openPath) || !isWindowsExecutable(openPath)
- modal.querySelector('#ow-missing').style.display = missing ? '' : 'none'
- const defWine = (state.settings && state.settings.defaultWine) || 'system'
- const defPrefix = state.prefixes.default || ''
- const wineOpts = [
- { value: 'default', label: ot('openwith.wineDefault', wineLabel(defWine)) },
- ...state.wines.map(w => ({ value: w.id, label: w.label }))
- ]
- const prefixOpts = [
- { value: '', label: ot('openwith.prefixDefault', defPrefix || 'Main') },
- ...state.prefixes.prefixes.filter(p => p.name !== defPrefix).map(p => ({ value: p.name, label: p.name }))
- ]
- new CustomSelect(modal.querySelector('#ow-wine'), wineOpts, 'default')
- new CustomSelect(modal.querySelector('#ow-prefix'), prefixOpts, '')
- modal.querySelector('#ow-cancel').onclick = closeModal
- modal.querySelector('#ow-run').onclick = async () => {
- if (missing) {
- closeModal()
- return
- }
- const runBtn = modal.querySelector('#ow-run')
- runBtn.disabled = true
- const res = await window.plwl.openwithRun({
- openPath,
- wine: modal.querySelector('#ow-wine').dataset.value || 'default',
- prefix: modal.querySelector('#ow-prefix').dataset.value || ''
- })
- closeModal()
- if (res.ok) {
- toast(t('toast.started', window.plwl.basename(openPath)), 'success')
- } else {
- const raw = res.error === 'file-missing' ? ot('openwith.fileMissing') : (res.error === 'not-windows' ? ot('openwith.notWindows') : (res.error || ''))
- toast(ot('openwith.startFailed', window.plwl.basename(openPath), raw), 'error')
- }
- }
-}
-
-// Entry point: the main process hands over a file opened through PLWL
-// (boot argv or a second-instance handoff).
-async function handleOpenWith(openPath) {
- if (!openPath) return
- await loadOpenwithTexts()
- showOpenWithModal(openPath)
-}
-
-function wireOpenWith() {
- // Both delivery paths (live handoff and the boot queue) converge on
- // openwithTake(): the main-process slot is drained exactly when a popup
- // shows, so a reloaded window can never resurrect an old popup and two
- // quick handoffs collapse into one (the newest file wins).
- window.plwl.onOpenWith(() => window.plwl.openwithTake().then(handleOpenWith))
- // file queued before the window existed (boot handoff): pick it up once
- window.plwl.openwithTake().then(handleOpenWith)
-}
-
// Import from a prefix, step 2: register the app in place - nothing is
// copied, paths stay prefix-relative.
async function doPrefixImport(prefix, srcDir) {
@@ -1183,7 +1075,6 @@ async function boot() {
applyI18n()
wireEvents()
wireRealtime()
- wireOpenWith()
qs('#loading').remove()
qs('#main').style.display = 'flex'
showView('library')
diff --git a/renderer/style.css b/renderer/style.css
index 2af013a..ab71bc6 100644
--- a/renderer/style.css
+++ b/renderer/style.css
@@ -627,36 +627,6 @@ select {
.modal .mode-buttons { display: flex; flex-direction: column; gap: 0.6rem; }
.modal .mode-buttons .button { justify-content: flex-start; }
-/* --- "Open with PLWL" picker ---------------------------------------------- */
-.modal .openwith-file-row {
- word-break: break-all;
- margin-top: -0.3rem;
-}
-.modal .openwith-file-row code {
- font-family: inherit;
- color: var(--text-color);
- background-color: rgba(255, 255, 255, 0.06);
- border-radius: 0.4rem;
- padding: 0.1rem 0.4rem;
-}
-.button:disabled {
- opacity: 0.45;
- pointer-events: none;
-}
-
-/* --- form fields inside modals (shared by every modal with inputs) --------- */
-/* .modal is a flex column with gap: 0.8rem. That gap ADDS to the field
- margins reused from the config forms (.field-label margin-top + control
- margin), so label->control spacing in modals ended up ~3x bigger than in
- .form-card (where block-layout margins collapse). Inside modals the gap
- owns the group spacing, so the repeated field styles collapse to the same
- rhythm as the config forms: label hugs its control (~0.4rem), groups stay
- 0.8rem apart. */
-.modal .field-label { margin-top: 0; margin-bottom: -0.4rem; }
-.modal .custom-select,
-.modal input,
-.modal textarea { margin: 0; }
-
.toasts {
position: fixed;
bottom: 1rem;
diff --git a/src/argv.js b/src/argv.js
deleted file mode 100644
index cf93cc0..0000000
--- a/src/argv.js
+++ /dev/null
@@ -1,57 +0,0 @@
-'use strict'
-
-// Shared command-line parsing for the launcher process itself. Both boot
-// (process.argv) and the single-instance handoff (second-instance argv) use
-// it, so a shortcut, a "Open with PLWL" file-open and Chromium's own injected
-// flags are handled the same way everywhere.
-//
-// Supported forms:
-// --launch | --launch= auto-start that library app
-// --open | --open= open an .exe with the wine picker
-// a bare existing-file path same as --open (plumbing for Exec %f)
-
-function firstNonFlagAfter(args, i) {
- // Chromium injects its own flags into the second instance's argv and they
- // can land BETWEEN the option and its value (e.g. `--launch
- // --allow-file-access-from-files 1790414564627`), so skip anything that
- // looks like a flag and take the first non-flag token.
- for (let j = i + 1; j < args.length; j++) {
- if (!args[j].startsWith('--')) return args[j]
- }
- return null
-}
-
-function findValue(args, names) {
- for (const name of names) {
- const i = args.indexOf(name)
- if (i !== -1) return firstNonFlagAfter(args, i)
- const eq = args.find(a => a.startsWith(name + '='))
- if (eq) return eq.slice(name.length + 1)
- }
- return null
-}
-
-// PLWL's own options must not swallow Chromium's flags (--flag=... style),
-// so anything after a flag that itself carries a value is skipped here by
-// looking only at the KNOWN option names above.
-function parseArgv(argv) {
- const args = Array.isArray(argv) ? argv : process.argv
- const launchId = findValue(args, ['--launch'])
- let openPath = findValue(args, ['--open'])
- if (!openPath && !launchId) {
- // "Open with" plumbing (Exec %f): the file path arrives as the only
- // non-flag argument - never pick a bare token when a real flag carries
- // the intent, and skip this process's own binary (electron dev mode
- // passes it as the first argument).
- const bare = args.find(
- a =>
- !a.startsWith('-') &&
- /\.(exe|msi|bat|cmd|lnk)$/i.test(a) &&
- !/(electron|olcxja\.plwl)(\.exe)?$/i.test(a)
- )
- if (bare) openPath = bare
- }
- return { launchId, openPath }
-}
-
-module.exports = { parseArgv, findValue }
diff --git a/src/ipc/games.js b/src/ipc/games.js
index 5c89a12..1679fa6 100644
--- a/src/ipc/games.js
+++ b/src/ipc/games.js
@@ -31,14 +31,6 @@ function createGamesService({ getWindow, onGamesChanged }) {
const isRunning = id => running.has(id)
const listRunning = () => [...running.keys()]
- // "Open with PLWL" handoff: queue the file for the renderer (picker popup
- // with wine version + prefix). Safe to call at any time - if the window is
- // not up yet, the renderer picks the file up right after boot.
- function openExternally(openPath) {
- require('../openwith').setPending(openPath)
- sendToWindow(getWindow(), 'openwith:file', openPath)
- }
-
function resolveWine(game) {
const system = { id: 'system', type: 'system', label: 'System Wine' }
const wanted = game.wine === 'default' ? (library.loadSettings().defaultWine || 'system') : (game.wine || 'system')
@@ -128,7 +120,7 @@ function createGamesService({ getWindow, onGamesChanged }) {
return importGameFromPrefix(prefix, srcDir)
}
- return { isRunning, listRunning, start, kill, runToolFor, import: importTo, importFromPrefix, setOnGameExit, openExternally }
+ return { isRunning, listRunning, start, kill, runToolFor, import: importTo, importFromPrefix, setOnGameExit }
}
module.exports = { createGamesService, killProcessGroup, sendToWindow }
diff --git a/src/ipc/index.js b/src/ipc/index.js
index ab720c8..19f8211 100644
--- a/src/ipc/index.js
+++ b/src/ipc/index.js
@@ -12,7 +12,6 @@ const runner = require('../runner')
const { extractIconFromExe } = require('../icons')
const { createGamesService, sendToWindow } = require('./games')
const shortcuts = require('../shortcuts')
-const openwith = require('../openwith')
const ICON_EXTS = ['png', 'jpe?g', 'svg', 'ico', 'webp', 'gif', 'bmp']
@@ -111,7 +110,6 @@ function registerIpc({ getWindow, onGamesChanged }) {
}
})
ipcMain.handle('games:shortcutPaths', (e, id) => shortcuts.shortcutPaths(id))
- ipcMain.handle('openwith:texts', (e, lang) => openwith.textsFor(lang))
ipcMain.handle('games:clearPlaytime', (e, id) => library.clearPlaytime(id))
// games: icons ---------------------------------------------------------------------
@@ -158,29 +156,6 @@ function registerIpc({ getWindow, onGamesChanged }) {
return extractGameIcon(id, game.exePath)
})
- // "Open with PLWL" ------------------------------------------------------------------
- // The renderer asks for the file handed in via `--open` (Exec %f), then
- // launches it with the wine/prefix picked in the popup. Nothing is imported
- // into the library.
- ipcMain.handle('openwith:take', async () => {
- const p = openwith.getPending()
- if (!p) return null
- openwith.clearPending()
- return p
- })
- ipcMain.handle('openwith:run', async (e, payload) => {
- try {
- return await openwith.run({
- openPath: payload && payload.openPath,
- wine: payload && payload.wine,
- prefix: payload && payload.prefix,
- lang: library.loadSettings().language
- })
- } catch (err) {
- return errorResult(err)
- }
- })
-
// games: run --------------------------------------------------------------------------
ipcMain.handle('games:run', (e, id) => games.start(getWindow(), id))
ipcMain.handle('games:kill', (e, id) => games.kill(id))
diff --git a/src/main.js b/src/main.js
index 15f2697..f66d261 100644
--- a/src/main.js
+++ b/src/main.js
@@ -7,8 +7,6 @@ const wines = require('./wines')
const { registerIpc } = require('./ipc')
const { createMainWindow } = require('./window')
const { setAppIcon } = require('./appicon')
-const { parseArgv } = require('./argv')
-const openwith = require('./openwith')
const path = require('path')
const fs = require('fs')
@@ -25,7 +23,6 @@ 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
@@ -53,10 +50,9 @@ if (!gotLock) {
} else {
app.on('second-instance', (e, argv) => {
trace(`second-instance argv=${argv.join(' ')}`)
- const { launchId, openPath } = parseArgv(argv)
+ const id = parseLaunchId(argv)
lastHandoffAt = Date.now()
- if (openPath) openWith(openPath)
- else if (launchId) launchGame(launchId)
+ if (id) launchGame(id)
else showMainWindow()
})
}
@@ -66,20 +62,24 @@ if (!gotLock) {
// 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)
+function parseLaunchId(argv) {
+ const args = Array.isArray(argv) ? argv : process.argv
+ const i = args.indexOf('--launch')
+ if (i !== -1) {
+ // Chromium injects its own flags into the second instance's argv and they
+ // can land BETWEEN --launch and the value (e.g. `--launch
+ // --allow-file-access-from-files 1790414564627`), so skip anything that
+ // looks like a flag and take the first non-flag token.
+ for (let j = i + 1; j < args.length; j++) {
+ if (!args[j].startsWith('--')) return args[j]
+ }
+ return null
+ }
+ const eq = args.find(a => a.startsWith('--launch='))
+ return eq ? eq.slice('--launch='.length) : null
}
-const auto = parseArgv(process.argv)
-autoLaunchId = auto.launchId
-autoOpenPath = auto.openPath
+autoLaunchId = parseLaunchId(process.argv)
headless = !!autoLaunchId
autoQuitWhenDone = headless // booting just to run one game -> exit when it ends
@@ -241,11 +241,6 @@ app.whenReady().then(async () => {
} 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())
- }
}
})
diff --git a/src/openwith.js b/src/openwith.js
deleted file mode 100644
index 57ec493..0000000
--- a/src/openwith.js
+++ /dev/null
@@ -1,108 +0,0 @@
-'use strict'
-
-// "Open with PLWL" (file manager -> Open With -> PLWL): a file handed in via
-// `--open ` (or the Exec %f plumbing) is parked here until the renderer
-// picks it up, then launched ad hoc with the wine version + prefix chosen in
-// the picker popup. Nothing is imported into the library.
-
-const fs = require('fs')
-const path = require('path')
-
-let pending = null // file path waiting for the picker (service not ready yet)
-
-function setPending(p) {
- pending = p
-}
-
-function getPending() {
- return pending
-}
-
-function hasPending() {
- return !!pending
-}
-
-function clearPending() {
- pending = null
-}
-
-// The launcher's own translations may not cover openwith texts in older
-// installs - fall back to English keys so the UI never shows raw key names.
-function textsFor(code) {
- let texts = {}
- try {
- texts = require('./i18n').getLang(code).texts || {}
- } catch {}
- const fallback = {
- 'openwith.title': 'Open with Wine',
- 'openwith.file': 'File',
- 'openwith.fileMissing': 'This file no longer exists.',
- 'openwith.wine': 'Wine / Proton version',
- 'openwith.wineDefault': 'Default ({0})',
- 'openwith.prefix': 'Wine prefix',
- 'openwith.prefixDefault': 'Default prefix ({0})',
- 'openwith.run': 'Run',
- 'openwith.startFailed': 'Could not start {0}: {1}',
- 'openwith.notWindows': 'This is not a Windows executable (exe/msi/bat/cmd/lnk).'
- }
- return { ...fallback, ...texts }
-}
-
-function resolveWine(id) {
- const system = { id: 'system', type: 'system', label: 'System Wine' }
- const wanted = id === 'default' ? require('./library').loadSettings().defaultWine || 'system' : id
- if (!wanted || wanted === 'system') return system
- return require('./wines').getWine(wanted) || system
-}
-
-function resolvePrefix(name) {
- const defs = require('./prefixes').listPrefixes()
- const known = name && defs.prefixes.some(p => p.name === name) ? name : ''
- return known || defs.default || 'Main'
-}
-
-// Ad-hoc run target: shaped exactly like a library game (runner.buildEnv and
-// the playtime/log paths expect those fields) but nothing is persisted.
-function makeTarget(openPath, wineId, prefixName) {
- const dir = path.dirname(openPath)
- return {
- id: 'openwith',
- name: path.basename(openPath),
- mode: 'open',
- path: dir,
- exePath: openPath,
- workdir: dir,
- wine: wineId || 'default',
- prefix: prefixName || '',
- args: '',
- dllOverrides: [],
- env: {},
- iconSource: '',
- playSeconds: 0,
- created: 0,
- lastPlayed: 0
- }
-}
-
-// Launch without importing: resolves the chosen wine/prefix, builds the ad
-// hoc target and runs it exactly like a library app (same env, same logging,
-// same detached process group).
-async function run({ openPath, wine: wineId, prefix: prefixName0, lang, onStarted }) {
- if (!openPath || !fs.existsSync(openPath)) return { ok: false, error: 'file-missing' }
- if (!/\.(exe|msi|bat|cmd|lnk)$/i.test(openPath)) return { ok: false, error: 'not-windows' }
- const wine = resolveWine(wineId)
- const prefixName = resolvePrefix(prefixName0)
- const runner = require('./runner')
- try {
- // runAdHoc builds the synthetic target internally (shared `openwith` id,
- // so logs/playtime never collide with a library app id); the resolved
- // prefix is passed through so buildEnv uses the chosen one
- const res = await runner.runAdHoc({ openPath, wine, systemWine: wine.type === 'system', prefix: prefixName })
- if (onStarted) onStarted({ name: path.basename(openPath), pid: res.pid })
- return { ok: true, pid: res.pid || null, logFile: res.logFile || null }
- } catch (err) {
- return { ok: false, error: String((err && err.message) || err) }
- }
-}
-
-module.exports = { setPending, getPending, hasPending, clearPending, run, makeTarget, resolveWine, resolvePrefix, textsFor }
diff --git a/src/paths.js b/src/paths.js
index b244087..71310c1 100644
--- a/src/paths.js
+++ b/src/paths.js
@@ -50,51 +50,17 @@ const DIRS = {
FAKE_STEAM: path.join(DATA, 'fake-steam')
}
-// Bundled translation files are launcher-owned: on every start they are
-// refreshed in the portable files/translations folder WHENEVER THEIR CONTENT
-// CHANGED, so an update shipping new or changed keys reaches existing
-// installs too (the old copy-only-if-missing logic froze translations at the
-// version that happened to run first). Extra files in the target folder that
-// the bundle does not ship (user-added languages) are left untouched.
-function filesDiffer(a, b) {
- try {
- return !fs.readFileSync(a).equals(fs.readFileSync(b))
- } catch {
- return true // target missing/unreadable -> needs a (re)copy
- }
-}
-
-function syncBundledTranslations(bundledDir, targetDir) {
- let names = []
- try {
- names = fs.readdirSync(bundledDir)
- } catch {
- return 0
- }
- let updated = 0
- for (const name of names) {
- const src = path.join(bundledDir, name)
- const dst = path.join(targetDir, name)
- try {
- if (!fs.statSync(src).isFile()) continue
- } catch {
- continue
- }
- if (!filesDiffer(src, dst)) continue
- try {
- fs.copyFileSync(src, dst)
- updated++
- } catch {}
- }
- return updated
-}
-
function ensureDirs() {
for (const dir of [FILES, DATA, DIRS.WINES, DIRS.PREFIXES, DIRS.APPS, DIRS.TRANSLATIONS, DIRS.FAKE_STEAM]) {
fs.mkdirSync(dir, { recursive: true })
}
if (DIRS.TRANSLATIONS !== DIRS.BUNDLED_TRANSLATIONS) {
- syncBundledTranslations(DIRS.BUNDLED_TRANSLATIONS, DIRS.TRANSLATIONS)
+ try {
+ for (const f of fs.readdirSync(DIRS.BUNDLED_TRANSLATIONS)) {
+ const dst = path.join(DIRS.TRANSLATIONS, f)
+ if (!fs.existsSync(dst)) fs.copyFileSync(path.join(DIRS.BUNDLED_TRANSLATIONS, f), dst)
+ }
+ } catch {}
}
}
@@ -129,4 +95,4 @@ function writeJson(file, data) {
fs.renameSync(tmp, file)
}
-module.exports = { RESOURCES, APP_ROOT, BUILD, FILES, DATA, DIRS, ensureDirs, syncBundledTranslations, readJson, writeJson, toPortablePath, resolvePortablePath }
+module.exports = { RESOURCES, APP_ROOT, BUILD, FILES, DATA, DIRS, ensureDirs, readJson, writeJson, toPortablePath, resolvePortablePath }
diff --git a/src/preload.js b/src/preload.js
index 0472793..fbecbb2 100644
--- a/src/preload.js
+++ b/src/preload.js
@@ -10,15 +10,6 @@ ipcRenderer.on('import:progress', (e, data) => {
if (importProgressCb) importProgressCb(data)
})
-// "Open with PLWL": single-slot callback for files handed over via --open
-// (Exec %f) - the renderer shows the wine/prefix picker popup for it.
-let openWithCb = null
-ipcRenderer.on('openwith:file', (e, openPath) => {
- if (openWithCb) openWithCb(openPath)
-})
-const fs = require('fs')
-const path = require('path')
-
contextBridge.exposeInMainWorld('plwl', {
FILES_DIR: DIRS.FILES,
i18nList: () => ipcRenderer.invoke('i18n:list'),
@@ -35,20 +26,6 @@ contextBridge.exposeInMainWorld('plwl', {
prefixesCreate: name => ipcRenderer.invoke('prefixes:create', name),
prefixesDelete: name => ipcRenderer.invoke('prefixes:delete', name),
prefixesSetDefault: name => ipcRenderer.invoke('prefixes:setDefault', name),
- openwithTake: () => ipcRenderer.invoke('openwith:take'),
- openwithRun: payload => ipcRenderer.invoke('openwith:run', payload),
- openwithTexts: lang => ipcRenderer.invoke('openwith:texts', lang),
- onOpenWith: cb => {
- openWithCb = cb
- },
- basename: p => (p ? path.basename(p) : ''),
- fileExists: p => {
- try {
- return !!p && fs.existsSync(p)
- } catch {
- return false
- }
- },
gamesList: () => ipcRenderer.invoke('games:list'),
gamesGet: id => ipcRenderer.invoke('games:get', id),
gamesUpdate: (id, patch) => ipcRenderer.invoke('games:update', id, patch),
diff --git a/src/runner.js b/src/runner.js
index 2dee951..24cf499 100644
--- a/src/runner.js
+++ b/src/runner.js
@@ -87,11 +87,9 @@ const GRACE_MS = 60000
// (a number of seconds) every 30s - apps.json is left untouched to spare the
// disk. lastPlayed is stamped once at start, final totals are mirrored into
// the registry once on exit.
-// `base` is the seconds total this run adds up to. It is resolved by the
-// CALLER from the live playtime file (not the registry) so ad-hoc runs that
-// share one id (see runAdHoc) never mix two apps' playtime.
-function startPlaytimeTicker({ game, startedAt, base }) {
+function startPlaytimeTicker({ game, startedAt }) {
const library = require('./library')
+ const base = game.playSeconds || 0
const timer = setInterval(() => {
const counted = Math.max(0, Math.round((Date.now() - startedAt - GRACE_MS) / 1000))
library.writePlaySeconds(game.id, base + counted)
@@ -147,10 +145,7 @@ function runGame({ game, wine, systemWine, onExit }) {
child.on('error', reject)
child.unref()
const startedAt = Date.now()
- // read the LIVE playtime file, not the (possibly shared/stale) registry
- // value - ad-hoc "open with" runs share one id and must not mix playtime
- const basePlaySeconds = require('./library').readPlaySeconds(game.id) ?? (game.playSeconds || 0)
- const stopTicker = startPlaytimeTicker({ game, startedAt, base: basePlaySeconds })
+ const stopTicker = startPlaytimeTicker({ game, startedAt })
// stamp "last played" once at start so it is correct even if we crash later
require('./library').updateGame(game.id, { lastPlayed: startedAt })
child.on('exit', (code, signal) => {
@@ -159,7 +154,7 @@ function runGame({ game, wine, systemWine, onExit }) {
const counted = Math.max(0, Math.round((Date.now() - startedAt - GRACE_MS) / 1000))
require('./library').updateGame(game.id, {
lastPlayed: startedAt,
- playSeconds: basePlaySeconds + counted
+ playSeconds: (game.playSeconds || 0) + counted
})
if (onExit) onExit(code, signal)
})
@@ -194,14 +189,4 @@ function runTool({ game, wine, tool, args, systemWine }) {
})
}
-// Ad-hoc launch ("Open with PLWL"): the exact runGame pipeline, but with a
-// synthetic target under the shared `openwith` id - log and playtime files
-// live in files/data/appdata/openwith/ and no library entry is created.
-// `prefix` carries the picker's choice ('' = launcher default, resolved by
-// buildEnv); wine/systemWine are the already-resolved build objects.
-function runAdHoc({ openPath, wine, systemWine, prefix, onExit }) {
- const game = require('./openwith').makeTarget(openPath, 'default', prefix || '')
- return runGame({ game, wine, systemWine, onExit })
-}
-
-module.exports = { buildEnv, runGame, runTool, runAdHoc, parseArgs, latestLogPath }
+module.exports = { buildEnv, runGame, runTool, parseArgs, latestLogPath }
diff --git a/tools/createshortcut.sh b/tools/createshortcut.sh
index 54a0d29..5eac756 100644
--- a/tools/createshortcut.sh
+++ b/tools/createshortcut.sh
@@ -24,20 +24,16 @@ if [ -f "$ICON_SRC" ]; then
fi
echo "2. Creating shortcut..."
-# MimeType + Exec %f: "Open with ..." in Dolphin/Nautilus lists PLWL for
-# Windows executables and hands the file path over as an argument (the
-# launcher then shows the wine/prefix picker).
mkdir -p "$APP_DIR"
cat > "$APP_DIR/olcxja.plwl.desktop" << EOF
[Desktop Entry]
Type=Application
Name=PLWL
Comment=Portable Linux Wine Launcher
-Exec="$EXE" %f
+Exec="$EXE"
Icon=olcxja.plwl
Terminal=false
Categories=Game;
-MimeType=application/x-ms-dos-executable;application/x-msi;application/x-msdownload;
EOF
chmod +x "$APP_DIR/olcxja.plwl.desktop"
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$APP_DIR" 2>/dev/null || true
diff --git a/tools/install.sh b/tools/install.sh
index 6d04b9b..8dab12f 100644
--- a/tools/install.sh
+++ b/tools/install.sh
@@ -40,19 +40,15 @@ if [ -f "$ICON_SRC" ]; then
fi
echo "3. Creating shortcut..."
-# MimeType + Exec %f: "Open with ..." in Dolphin/Nautilus lists PLWL for
-# Windows executables and hands the file path over as an argument (the
-# launcher then shows the wine/prefix picker).
cat > "$APP_DIR/olcxja.plwl.desktop" << EOF
[Desktop Entry]
Type=Application
Name=PLWL
Comment=Portable Linux Wine Launcher
-Exec="$EXE" %f
+Exec="$EXE"
Icon=olcxja.plwl
Terminal=false
Categories=Game;
-MimeType=application/x-ms-dos-executable;application/x-msi;application/x-msdownload;
EOF
chmod +x "$APP_DIR/olcxja.plwl.desktop"