155 lines
9.6 KiB
Markdown
155 lines
9.6 KiB
Markdown
# 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
|
|
lansync.js LAN sync handlers (identity, devices, re-roll, sync + progress)
|
|
runner.js spawns wine/proton (env, prefix, playtime, logs)
|
|
library.js apps registry (apps.json) + settings + playtime files, plus
|
|
the "which prefix does this app use" / in-use guards
|
|
wines.js wine/proton build scan, registry, portable symlink healing
|
|
prefixes.js prefix registry (files/wineprefixes)
|
|
symlinks.js relative symlink rewiring + executable-bit restoration
|
|
(shared by imports, prefixes and LAN sync)
|
|
fsutil.js tree copy/remove with byte progress + rate-gated progress
|
|
events (imports, prefix/wine/app deletion, LAN sync)
|
|
importer.js app import (copy/move/leave/prefix)
|
|
names.js user-typed name sanitizing (prefix folder names, wine
|
|
display names)
|
|
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
|
|
lansync.js LAN sync: identity + pairing, hashing, HTTP receiver (with its
|
|
receive-progress hook), UDP discovery
|
|
lansyncpush.js the pushing side (createPusher factory, keeps helper state out)
|
|
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/<name>.test.js`;
|
|
electron test/<name>.test.js for ones that need the real IPC/Electron
|
|
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).
|
|
- A prefix or wine build that a running app uses cannot be renamed/deleted
|
|
(wine holds its registry and user files open in that folder): the guards sit
|
|
next to the handlers in src/ipc/, use the games service's running list plus
|
|
the helpers in library.js (prefixNameForGame/gamesUsingPrefix/is*InUse) and
|
|
answer { ok:false, error:'inUse'|'hasApps', apps:[names] } for the toast.
|
|
- Long operations stream progress through the single import:progress slot as
|
|
{ phase: 'count'|'copy'|'remove', files, bytes, totalFiles, totalBytes } and
|
|
the renderer drives a bar from it (runWithProgress for deletes/imports).
|
|
Emit the 'count' phase first (a treeSize walk) and copy/remove through the
|
|
fsutil helpers; never ship a progress bar no data can move.
|
|
- 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.
|
|
- LAN sync has two sides: the receiver lives in lansync.js, the sender in
|
|
lansyncpush.js and is built by createPusher(helpers) - several launcher
|
|
states share the module, so nothing may be kept in its module scope. Both
|
|
sides report their own progress (the receiver through the onReceive hook),
|
|
so each machine can show the transfer.
|
|
- An incoming sync needs CONSENT: a correct code opens the gate, but the
|
|
receiver's onIncoming hook (registered in main.js) shows an Accept/Decline
|
|
popup and only a click - answered through lansync:respondIncoming - starts
|
|
the session. Unanswered requests expire after INCOMING_TIMEOUT_MS; a decline
|
|
returns 'rejected' to the sender and KEEPS the pairing (the code was proven).
|
|
Without an onIncoming hook (tests, headless) the gate stays open.
|
|
- Sync progress is bytes plus per-category counters (items = announced units,
|
|
itemsDone = finished ones, totals = everything the sender picked), never raw
|
|
file counts: the popup says "Apps: 2/5". The bar follows the CURRENT PHASE's
|
|
bytes (barBytes/barTotalBytes, not the overall bytes/totalBytes) so a phase
|
|
starts from an empty bar instead of inheriting a full one - otherwise hashing
|
|
looks finished the moment it starts. Hashing and copying walk tens of
|
|
thousands of files, so every per-file update goes through progressGate
|
|
(~10/s, phase changes forced) - never send one IPC message per file.
|
|
- The launcher's own defaults are opt-in (the "defaults" sync category):
|
|
payload.defaults carries the default wine and prefix, and the receiver only
|
|
applies values that exist there (an unknown wine id or prefix is skipped).
|
|
- LAN sync remembers file hashes in files/data/lansync-hashes.json, keyed by
|
|
path with size + mtime (machine-local, never synced): equal size and mtime
|
|
means equal bytes, so a repeat sync only stat's the tree. Hashing and the
|
|
receiving side's comparison both run HASH_CONCURRENCY files at a time.
|
|
- A modal showing a running operation (a sync) opens locked:
|
|
showModal(html, { locked: true }) ignores the backdrop and Escape, because
|
|
the user cannot cancel the operation by dismissing it.
|
|
- Symlinks inside everything PLWL owns (a wine/proton build, a prefix, an
|
|
imported app) are stored RELATIVE whenever the target can be expressed
|
|
locally: a Proton prefix is full of links into its build, and an absolute one
|
|
points at the machine that wrote it. symlinks.js owns this (rewireSymlinks,
|
|
plus restoreExecutableBits for the executables a content copy drops) and
|
|
every writer calls it: importWineFolder/healBuild, importer after copy/move,
|
|
healPrefix, and the sync both on the wire (portableLinkTarget sends the
|
|
relative target) and after a unit lands (healUnitRoots). dosdevices drive
|
|
mappings and targets that leave files/ (a mount, a system path, the user's
|
|
home) stay absolute - they are machine-specific by design.
|
|
- Wine and Proton create prefix links with ABSOLUTE local targets whenever they
|
|
update a prefix (font/dll stubs, Steam appcache), so on-disk links are not
|
|
always relative after a run: the sync payload relativizes them anyway, and
|
|
refreshWines()/healPrefix() repair what arrived from an older PLWL. A build
|
|
whose binaries lost their executable bit is detected by a cheap audit in
|
|
refreshWines() and healed once, because spawn() on a non-executable `proton`
|
|
fails with EACCES and the game would look like it started while nothing ran.
|
|
- File permissions travel with a sync (manifest entry `mode`): the receiving
|
|
side applies them after writing the bytes and repairs a file that has the
|
|
same hash but the wrong mode without re-uploading it.
|
|
- 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 <changed js files> # syntax
|
|
node test/import.test.js # ...and the other test files
|
|
node test/lansync.test.js # LAN sync contract (identity, pairing, mirror)
|
|
node test/lansync-receiving.test.js # receive-side progress events (the popup)
|
|
node test/lansync-settings.test.js # playtime, app settings, wine/prefix + defaults
|
|
node test/lansync-links.test.js # wire: exec bits + relative links in a sync
|
|
node test/lansync-pending-links.test.js # links to files created later, startup heal
|
|
node test/lansync-consent.test.js # incoming sync: accept / decline / timeout gate
|
|
node test/bench-sync.js # timing: phases of a cold and a cached sync
|
|
npm start # click through the flow you touched
|
|
|
|
# Repairing / testing a real instance (test/ scripts are gitignored helpers):
|
|
PLWL_BUILD_DIR=/path/to/instance node test/heal-instance.js # links + exec bits
|
|
PLWL_BUILD_DIR=/path/to/instance node test/launch-instance.js <id> # really start a game
|
|
```
|