- JavaScript 89.9%
- CSS 6%
- HTML 2.6%
- Shell 1.5%
| files/translations | ||
| node_modules | ||
| renderer | ||
| src | ||
| tools | ||
| .gitignore | ||
| build.sh | ||
| development.md | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| run.sh | ||
PLWL — Portable Linux Wine Launcher
A portable launcher for Windows games and applications on Linux — and for native Linux apps, run with their own home and a chosen isolation level (bubblewrap when available).
Portable layout
Everything the launcher needs lives next to the executable:
./files/
wines/ imported Wine / Proton builds
wineprefixes/ wine prefixes (a native Linux app's data lives in <prefix>/linux_pfx)
apps/ imported app folders (optional)
data/ launcher config (apps.json, settings.json)
translations/ translation files
Running
npm install
npm start
Building & installing
./build.sh produces build/PLWL-<version>.tar.gz: the Electron runtime renamed to olcxja.plwl/ (binary olcxja.plwl/olcxja.plwl, whole app in olcxja.plwl/resources/app.asar), plus the icon, install.sh and createshortcut.sh.
./install.sh # install to ~/.local/bin/olcxja.plwl/ (icon + shortcut)
./createshortcut.sh # portable mode: icon + shortcut only
./olcxja.plwl/olcxja.plwl # or run without installing
Importing from a wine prefix
Besides copy/move/leave, an app can be imported from a prefix: nothing is copied — paths are stored relative to the prefix, so wine sees the app as installed on C:\ (registry entries keep working). Plain prefixes (drive_c on top) and Proton ones (pfx/) both work. The app stays tied to that prefix; removing the entry never deletes prefix files. A prefix can be renamed later — the folder moves and everything stored relative follows. Edits wait for running apps: renaming/deleting a prefix in use (or moving the default away from an app without its own) is refused with the app to close first; deleting a prefix that still has apps is refused too. Imports recreate symlinks (relative when the target belongs to the launcher) and keep permissions, so an imported game runs straight away.
Native Linux apps
The import dialog starts with a choice: Windows app (Wine / Proton) or Linux app (native) — after that both go through the same copy/move/leave steps.
The entry point is found automatically in the picked folder: an ELF binary or .AppImage (with the executable bit), or a .desktop entry whose Exec= points at one. Libraries (lib/, share/, …) are skipped, and the icon comes from the .desktop Icon=, an image next to the program, or the system theme.
A native app runs with its own home folder: HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME and XDG_STATE_HOME point into files/wineprefixes/<prefix>/linux_pfx/, so config, cache and state land there instead of in your ~. The prefix select in the app's config picks which one; Open data folder takes you to it. linux_pfx is part of its prefix like everything else: a sync carries it with the prefix — Linux apps and their data travel in the same categories as the Windows ones.
When bubblewrap is installed and usable (PLWL probes it with a real run, not which bwrap — Ubuntu's AppArmor profile kills it even when it is installed), the two sandboxed levels also give the app its own namespaces: / is mounted read-only, so only its home folder and its own app folder stay writable — anything else fails with EROFS. Relaxed adds exactly one mount on top of that profile (the writable host filesystem described below); everything else on this page is the same at both levels. The app folder is included on purpose: portable apps keep their logs and data next to themselves (Prism Launcher refuses to start otherwise). System trees are never handed out writable, not even when the app itself lives in one. Screens, audio (X11/Wayland, PulseAudio/PipeWire), gamepads and networking keep working. XDG_RUNTIME_DIR stays the host's, so the session bus is shared — the sandbox is not airtight, and the UI says so rather than pretending.
A file your desktop hands the app — a drag-and-drop from your file manager, a file picked in a file dialog — is named by its real path (/home/you/Downloads/mod.jar), and inside the sandbox that name is the app's own redirected home. At Relaxed your home folders are linked back into the app at those very paths, through the writable mount, so the drop opens where it lies and a save lands on your real file; the one name left alone is the folder the app itself lives in, which keeps its plain path (a link there turned a launcher app's own instances into a /tmp path, which Prism Launcher refuses to see as permanent). At Isolated there is no view to link into, and what reaches the app there is only what the launcher hands it, as a copy (see the levels below).
The redirected home starts with a copy of your desktop preferences — kdeglobals, the GTK 3/4 settings and user-dirs.dirs — so the app keeps your theme instead of falling back to its built-in light look. The copy keeps following your host as long as the app has not changed it: a theme change arrives on the next launch, and the moment the app writes its own version, PLWL never touches it again. Your own fonts, icons, themes and colour schemes are not copied but mounted read-only inside the sandbox and added to XDG_DATA_DIRS, so they follow the host live and nothing can be written back to your ~.
Anything the app wants to open is opened by your own desktop. A link goes to the host's xdg-desktop-portal, so it lands in the browser you actually chose instead of one resolved in here, where the default association and its .desktop file are behind the redirected home — that used to mean a system browser you never picked, or nothing at all. Both routes reach the portal: xdg-open itself (what Qt, Prism and Electron call) and GIO's in-process resolution (gtk_show_uri). A folder or a document (Prism's Open Pack Folder, a text file, a drop) takes the other route: the app leaves the target in its own home, PLWL picks it up, translates the path back out of the sandbox and opens it with your xdg-open — so it arrives in your file manager, and a scheme nothing handles gets your desktop's own Choose an application dialog rather than a guess. On a machine without a portal nothing is changed — a link then behaves exactly as it did before. A KDE-native app that resolves the link in-process through KIO (Dolphin, Kate) is the one path still left out: it asks KDE's own handler database, which has no answer in here (measured: kde-open opens nothing and reports success).
Every prefix in the Wine prefixes tab has a Reset Linux config button: it deletes the desktop preferences PLWL copied in (and the hashes that track them), so the next app start seeds a clean copy from your system — the way back when an app has hijacked a seeded file with its own theme. App data in linux_pfx is never touched, and an app running from that prefix blocks the reset.
Imported trees get normalised permissions: every file gets the owner's rwx and an x mirrored from its r, because sources come from archives, FAT sticks or read-only backups where those bits are simply not there. An import whose copy could not reproduce every file is refused and the partial copy is removed, so a half-copied app is never registered.
Some apps draw their window icon themselves, which Wayland does not allow — Minecraft (LWJGL 3.4+) is the known case: its taskbar icon turns into the generic Wayland one. The per-app Force X11 (XWayland) switch moves the app and the programs it launches back to X11, where the icon works again.
Per app you choose the Isolation level — what the app gets besides its own home folder:
- None — a plain run. The app sees your real filesystem exactly as any other program of yours does: what a shortcut hands it works, including into an app that is already running.
- Relaxed — the sandbox plus your whole filesystem mounted writable at
/tmp/plwl-fs-root. A document handed over behaves natively — it opens where the app was told it is, it can be saved, and it also reaches an app that is already running, because the mount is there from the first second of every run. Your home folders are linked back into the app through that mount, so a file you drag onto the app, or pick in its own dialog, opens at its real path too. The price is real and the UI says it: at this level the app can change anything your user can. - Isolated — the sandbox without that mount. The system stays read-only, and a document handed over is copied first, into
plwl-handoffinside the app's own home folder; the app works on that copy (so the original cannot be touched), and a copy big enough to be noticed shows a progress popup in the launcher. There is no view of your home here, so a drop from your desktop or a file picked in a dialog cannot reach the app — only what the launcher hands over does, as that copy. - Automatic (default) — Relaxed where bubblewrap works, None where it does not.
An explicit Relaxed or Isolated on a machine without a working bubblewrap refuses to start instead of quietly dropping the promise; Automatic is the one level allowed to weaken itself. What the app really ran as is always visible: a badge on the library card, a [plwl] isolation: … line in the app log, and a status card in Settings with Check again. When bwrap is missing, PLWL says so once instead of quietly launching without it.
"Open with PLWL"
The desktop entry registers .exe/.msi/.bat/.cmd/.lnk in file managers. Opening one shows a popup to pick the Wine/Proton build and prefix for that run (defaults preselected, nothing imported). A second instance hands the path to the running one. App shortcuts work as Open With targets too: the app receives the file as the Windows path it would get on Windows — C:\... inside its prefix, Z:\... otherwise. A file handed to an already running app goes to that instance instead of a second tracked run. A Linux app shortcut is an Open With target the same way, and what the app is handed depends on its isolation level: at None the plain absolute path, at Relaxed the same file through the writable host mount (/tmp/plwl-fs-root/…) — which is also the only shape an app that is already running can resolve, since a mount cannot be added to a live sandbox — and at Isolated a copy in plwl-handoff inside the app's own home folder, so the original stays untouched.
LAN syncing
Two PLWL installs on one LAN can mirror each other (sidebar → LAN syncing). No cloud, no accounts.
- Device ID (24 chars) and Sync code (8 chars, letters+digits, case-insensitive), both re-rollable. A caller must prove it knows its own code; each verification costs 2 s and one may run at a time, so guessing is pointless. A correct code still opens an Accept/Decline popup on the receiver (60 s timeout); declining or ignoring never touches this machine and keeps the pairing.
- Devices on your network are found over UDP and visible only while PLWL runs there (a few seconds after it closes they are gone).
- Pick what to push: portable apps, prefix-imported apps, Wine/Proton builds, prefixes, Sync settings. Only what is ticked travels — without Sync settings the other machine's defaults and app configs are untouched. An accepted code is stored with the device ID, so later syncs need no typing; a re-rolled code is forgotten and asked for again.
Both machines show a progress popup (phase, unit, files, per-category counters like Apps: 2/5, transferred vs. planned bytes; the bar follows the current phase). The popups cannot be dismissed, and a sender hashing a big unit shows Waiting for the other device… instead of a dead popup — the sender pings while it works. They close on completion, or after 15 minutes of silence (and reopen if the sync turns out to be alive).
The sender is the source of truth: differing files are overwritten, missing ones written, extras inside the synced trees (files/apps/, files/wines/, files/wineprefixes/, files/data/icons/) deleted. A tree the sender could not fully read is the exception: that unit is still written, but nothing on the other side is deleted for it, because a missing entry may only mean it was unreadable. Only one sync runs at a time (a second attempt on the same machine answers busy), and a transfer cut off mid-file leaves no half-written file behind. Files are compared with BLAKE2b-512 (SHA-512/SHA-256 fallback); Force sync all files skips the comparison. Hashes are cached per file (size + mtime), so an unchanged library costs nothing to re-sync. Permissions travel with the files, and symlinks are sent as relative paths whenever the target belongs to the launcher (a Proton prefix's links into its build keep working on the other machine); dosdevices and targets outside files/ stay absolute, and links wine itself rewrote to absolute are relativized again on the wire, on import and before every run.
A sync also carries how apps are set up: wine build and prefix, args, env, DLL overrides, a native Linux app's isolation level and Force X11, playtime and last played. Sync settings carries the registry-level part even when no files move: default wine/prefix, custom build names, prefix registry, every app's configuration and the library order. It only patches what already exists (unknown ids are ignored, unknown apps/prefixes stay), deletes nothing, and playtime only grows — the higher counter wins. Language, window settings, logs and external (leave) apps stay local.
Wine & Proton support
- System Wine —
winefrom PATH. - Imported builds — drop any wine/Proton build into
files/wines/(one nesting level auto-detected). Symlinks are rewired to relative and executable bits restored, so builds from a zip or another machine run as they are. A build can get a display name of its own; the folder keeps its name. - Proton without Steam — invoked like Steam does it:
STEAM_COMPAT_DATA_PATHlinked to the launcher's prefix,STEAM_COMPAT_CLIENT_INSTALL_PATHpointing at a real Steam client when there is one (read-only) and at the emptyfiles/data/fake-steamotherwise. GE builds run through Proton's UMU path (UMU_ID) with no Steam client at all. A per-app Disable UMU switch runs the build's own wine directly (runinprefix), as close to plain wine as it gets. - One C: drive per prefix — when a prefix has no
pfxyet, PLWL makespfxa relative symlink back to the prefix itself, so Proton and wine share onedrive_c, registry anddosdevices. Existing layouts (a realpfx/folder, a prefix with both trees) are left exactly as they are.
Translations
All UI text comes from JSON files in files/translations/: data holds the language names, texts maps keys to strings with {0}, {1} ... placeholders. The bundled files are refreshed into that folder on every start when their content changed (so updates deliver new keys); user-added languages are never touched.
Development
Codebase overview and contributor rules: development.md.