Desktop is CachyOS with Hyprland on an NVIDIA card, three monitors. Windows games run through Lutris, not Steam; Steam is only there for native Linux games like Dota 2.
The stack
- Lutris as the library, with umu as the launcher and proton-cachyos as the Proton build — the CachyOS package, so it updates with the system.
- ntsync is on out of the box (CachyOS kernel), so no esync/fsync fiddling.
- VRR only for fullscreen windows.
- The library is split into two Lutris categories, Flat and VR — the VR side has its own page.
A workspace per game
Every umu/Proton game gets the window class steam_app_default, real Steam games get
steam_app_<id>. One Hyprland rule matching both makes every game fullscreen; a second one excludes
launchers and installers so those stay windowed.
On top of that, a small Lua handler in my Hyprland config gives every game launch its own fresh workspace on the center monitor. When the game closes, the workspace disappears with it. Two games never land on top of each other, and nothing needs a per-game rule.
Native Linux Steam games are the catch: they keep their own window class (Dota 2 is literally
dota2). But Steam puts SteamAppId into the environment of everything it launches, so the handler
also reads /proc/<pid>/environ and catches those too.
Installing is a right-click
An install-game script does the whole thing in one go: unpack, create the Lutris entry, find the
game on lutris.net for the cover art, add a launcher entry, add it to the streaming list. It sits in
the file manager’s right-click menu on game archives. uninstall-game on a game folder removes all
of it again and reports the freed space.
One thing I learned the hard way: Lutris rewrites its database when it exits. Any script that edits the database has to close Lutris first, or Lutris quietly restores the old rows.
The Battle.net corner
WoW, Hearthstone and Battle.net share one prefix, and Battle.net is the fussiest part of the setup:
- Tray icon. Wine apps use old-style XEmbed tray icons; my bar only speaks the newer
StatusNotifier protocol.
xembedsniproxybridges the two. Exactly one instance may run. - It never really exits. Closing Battle.net from the tray leaves its update agent running, which keeps the prefix alive — and the next game launched into that live session gets a fake GPU. A post-exit hook in Lutris now shuts the prefix down properly.
- It crashed once on every launch. An old
--runonstartupargument from the Lutris install script swallowed the next argument. Deleting it fixed a crash dialog I’d been clicking away for weeks. - WoW region. My game files are the EU build. “Locate game” refused them; “Install” pointed at the existing folder adopted them and only patched the difference.
- WoW beta. Launched by hand with
PROTON_VERB=run— the default verb hangs while Battle.net holds the prefix. It also hung the GPU on entering the world, until I tried entering at the lowest graphics preset and raising it afterwards. - Hearthstone sometimes loses permissions on its own game folder. A one-line
chmodas Lutris pre-launch command fixes it before every start.
Per-game fixes worth knowing
- Binding of Isaac saw my controller twice and scrambled the sticks. Disabling the duplicate
DirectInput device in the prefix’s registry fixed it. Disabling
dinput8entirely does not — then the game won’t start. - Dota 2 runs natively through Steam, with Minify for mods and cosmetics.
Streaming to the living room
The desktop runs Sunshine (started only when I need it); the living-room PC runs Moonlight.
- When a stream starts, a prep script turns off the two side monitors and switches the main one to 60 Hz. When it ends, the full layout comes back.
- The stream stays at native resolution and Sunshine scales it down for the TV. My main monitor can’t actually display a lower resolution, and games rendering at native look the same streamed or local.
- Every Lutris game is in Sunshine’s app list with its cover art. A small wrapper keeps Sunshine attached to the running game, so “quit app” in Moonlight really closes it.
The living-room box
A separate mini PC running Bazzite in Gaming Mode, driven entirely by controller:
- Boots straight into a library with just three tiles. Getting there took a Decky plugin for the tab order and a small script that navigates Steam’s UI through its debug port once it has loaded.
- YouTube is the real TV app (youtube.com/tv) in a kiosk Firefox with a smart-TV user agent and an ad blocker, mapped to the controller with a custom Steam Input layout.
- The Xbox controller wakes it from suspend.
- Night mode audio: a PipeWire filter chain with a compressor, so explosions don’t jump and dialogue stays audible at low volume. Left and right are swapped, because the soundbar sits behind the couch.