From 944d8f86c2438b540edd4f2963e1e43aea2f1879 Mon Sep 17 00:00:00 2001 From: olcxja Date: Wed, 30 Sep 2026 19:16:52 +0200 Subject: [PATCH] add dev rules --- README.md | 5 ++++ development.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ src/appicon.js | 15 ++-------- src/ipc/games.js | 5 +--- src/main.js | 3 +- src/openwith.js | 5 +--- src/paths.js | 18 +++++++++++- src/shortcuts.js | 3 +- src/wines.js | 12 +++++++- 9 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 development.md diff --git a/README.md b/README.md index 0a88a00..76d302b 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,8 @@ The desktop entry (`MimeType` + `Exec %f`) makes PLWL show up in Dolphin/Nautilu 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. + +## Development + +Codebase overview, conventions and rules for contributors: [development.md](development.md). + diff --git a/development.md b/development.md new file mode 100644 index 0000000..8e54565 --- /dev/null +++ b/development.md @@ -0,0 +1,73 @@ +# Development + +Short guide for anyone working on the PLWL codebase. + +## Layout + +``` +src/ main process (Node/Electron backend) + main.js entry point: tray, single-instance handoff, --launch / --open + argv.js command-line parsing shared by boot and second-instance handoff + window.js main window creation + ipc/ all ipcMain handlers + index.js handler registration, icon protocol, dialogs + games.js games service: run/stop state, wine resolution, open-with handoff + runner.js spawns wine/proton (env, prefix, playtime, logs) + library.js apps registry (apps.json) + settings + playtime files + wines.js wine/proton build scan, registry, portable symlink healing + prefixes.js prefix registry (files/wineprefixes) + importer.js app import (copy/move/leave/prefix) + openwith.js "Open with PLWL": ad-hoc runs, no library entry + shortcuts.js .desktop shortcut generation + paths.js portable layout, FILES-relative path helpers, translation sync + i18n.js translation loading + {0} placeholder filling + preload.js contextBridge API exposed to the renderer + appicon.js window/taskbar icon +renderer/ UI (plain HTML/CSS/JS, no framework) + app.js views, modals, custom-select, all renderer logic + index.html static markup for every view + style.css the whole look; CSS variables at the top +files/ portable data the launcher ships/creates at runtime + translations/ en-us.json, pl.json (copied to the data dir on start) +test/ plain node scripts, run them with `node test/.test.js` +tools/ install.sh / createshortcut.sh / build helpers +``` + +Main process vs renderer: the renderer never touches Node/Electron directly - +everything goes through the `window.plwl` bridge (preload.js) and ipcMain +handlers in src/ipc/. Keep new APIs in that pattern. + +## Rules + +- Keep it simple. Plain functions over classes, no abstractions "for later". +- Comments explain the WHY (quirks, workarounds, races), not the WHAT. If the + code says it, don't repeat it in words. No comments at all when nothing + needs explaining. +- No duplicate logic: the second time you copy something, move it into a + helper (a shared module in src/ or a function next to its users) and call + it from both places. +- Everything user-visible goes through the translation files (en-us.json, + pl.json) with `{0}`, `{1}` placeholders. No hardcoded strings in app.js. +- Paths stored on disk are FILES-relative (portable layout): write with + toPortablePath(), read with resolvePortablePath() - never store absolute + paths for anything inside files/. +- Registries (apps.json, wines.json, prefixes.json) are rewritten whole via + writeJson(); playtime lives in its own tiny file so the registry is not + rewritten while a game runs. +- Deleting user data needs a confirm modal; best-effort cleanup is + try/catch-and-ignore with a comment saying why it is safe to ignore. +- Windows apps run through runGame()/buildEnv() - never spawn wine directly + somewhere else (prefix handling, Proton env and playtime live there). +- CSS: reuse the existing variables/classes (.button, .field-label, + .custom-select, form-card); spacing rhythm comes from --button-margin and + the flex gap of the container, not per-element magic numbers. +- Tests are plain assert-by-print scripts (see test/): set PLWL_BUILD_DIR to + a temp dir, check(), exit 1 on failure. Add one when fixing a bug. + +## Verify before you commit + +``` +node --check # syntax +node test/import.test.js # ...and the other test files +npm start # click through the flow you touched +``` diff --git a/src/appicon.js b/src/appicon.js index c9738c2..314cd90 100644 --- a/src/appicon.js +++ b/src/appicon.js @@ -1,20 +1,9 @@ 'use strict' -const path = require('path') -const fs = require('fs') - -function findPng() { - const p = path.resolve(__dirname, '..', 'renderer', 'plwl.png') - try { - fs.accessSync(p) - return p - } catch { - return null - } -} +const { launcherIconPath } = require('./paths') function setAppIcon(app) { - const png = findPng() + const png = launcherIconPath() if (!png) return try { app.setIcon(png) diff --git a/src/ipc/games.js b/src/ipc/games.js index 5c89a12..70f215b 100644 --- a/src/ipc/games.js +++ b/src/ipc/games.js @@ -40,10 +40,7 @@ function createGamesService({ getWindow, onGamesChanged }) { } function resolveWine(game) { - const system = { id: 'system', type: 'system', label: 'System Wine' } - const wanted = game.wine === 'default' ? (library.loadSettings().defaultWine || 'system') : (game.wine || 'system') - if (!wanted || wanted === 'system') return system - return require('../wines').getWine(wanted) || system + return require('../wines').resolveWine(game.wine || 'system') } function handleExit(id) { diff --git a/src/main.js b/src/main.js index 15f2697..75a28d7 100644 --- a/src/main.js +++ b/src/main.js @@ -87,8 +87,7 @@ autoQuitWhenDone = headless // booting just to run one game -> exit when it ends // Lives as long as the app does: shows "active in background" even when the // window is closed, and offers running games + Quit. function trayIconPath() { - const png = path.resolve(__dirname, '..', 'renderer', 'plwl.png') - return fs.existsSync(png) ? png : undefined + return require('./paths').launcherIconPath() || undefined } function createTray() { diff --git a/src/openwith.js b/src/openwith.js index 57ec493..fb256a1 100644 --- a/src/openwith.js +++ b/src/openwith.js @@ -49,10 +49,7 @@ function textsFor(code) { } 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 + return require('./wines').resolveWine(id) } function resolvePrefix(name) { diff --git a/src/paths.js b/src/paths.js index b244087..24656ae 100644 --- a/src/paths.js +++ b/src/paths.js @@ -114,6 +114,22 @@ function resolvePortablePath(p) { return path.join(FILES, p) } +// The launcher's own icon (renderer/plwl.png), used by the tray, the app +// icon and as the .desktop shortcut fallback. Cached: it never moves at +// runtime. +let launcherIcon = null +function launcherIconPath() { + if (launcherIcon === null) { + const p = path.join(APP_ROOT, 'renderer', 'plwl.png') + try { + launcherIcon = fs.existsSync(p) ? p : '' + } catch { + launcherIcon = '' + } + } + return launcherIcon || null +} + function readJson(file, fallback) { try { return JSON.parse(fs.readFileSync(file, 'utf8')) @@ -129,4 +145,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, syncBundledTranslations, launcherIconPath, readJson, writeJson, toPortablePath, resolvePortablePath } diff --git a/src/shortcuts.js b/src/shortcuts.js index 0718b3b..aa69016 100644 --- a/src/shortcuts.js +++ b/src/shortcuts.js @@ -63,8 +63,7 @@ function buildDesktopEntry(game) { } catch {} if (!icon) { // fall back to the launcher icon so the shortcut is never naked - const png = path.resolve(__dirname, '..', 'renderer', 'plwl.png') - if (fs.existsSync(png)) icon = png + icon = require('./paths').launcherIconPath() || '' } const lines = [ '[Desktop Entry]', diff --git a/src/wines.js b/src/wines.js index fc761cd..abdece1 100644 --- a/src/wines.js +++ b/src/wines.js @@ -156,6 +156,16 @@ function getDefaultWine() { return require('./library').loadSettings().defaultWine || 'system' } +// Resolve the wine build a run should use: 'default'/'system'/unknown all +// fall back to System Wine. Shared by the games service and the ad-hoc +// "open with" runner. +function resolveWine(id) { + const system = { id: 'system', type: 'system', label: 'System Wine' } + const wanted = id === 'default' ? getDefaultWine() : id + if (!wanted || wanted === 'system') return system + return getWine(wanted) || system +} + function setDefaultWine(id) { const wines = getWines() if (id !== 'system' && !wines.some(w => w.id === id)) return { ok: false, error: 'notFound' } @@ -276,4 +286,4 @@ async function importWineFolder(srcDir) { return { ok: true, path: toPortablePath(dst), symlinksFixed } } -module.exports = { refreshWines, getWines, getWine, setWineDisabled, getDefaultWine, setDefaultWine, deleteWine, importWineFolder, findWineBinary, detectType, fixBuildSymlinks } +module.exports = { refreshWines, getWines, getWine, resolveWine, setWineDisabled, getDefaultWine, setDefaultWine, deleteWine, importWineFolder, findWineBinary, detectType, fixBuildSymlinks }