feat(foxtrot): controller-only couch gaming (Steam + gamescope session + greetd greeter) #666

Open
lytedev wants to merge 9 commits from foxtrot-couch-gaming into main
Owner

Turns foxtrot into a SteamOS-like couch-gaming machine driven entirely by the
Steam Controller (lid closed), without compromising its primary role as a
laptop. Squashes the previously-stacked #656 / #665 / #664 into one changeset,
rebased onto current main.

Steam migration (flatpak → programs.steam)

The Flatpak Steam's sandbox always binds the host (niri) display sockets, so a
host gamescope could never contain it — Steam escaped onto niri. nix-managed
Steam embeds in gamescope properly. 332G library + native saves moved in place
via btrfs rename (no re-download).

Seated "Gaming (gamescope)" wayland-session + controller exit

A session the display manager launches on its own DRM seat, so gamescope owns
input and Steam Input drives the cursor natively (the Steam Deck model) — the only
way to get controller-as-mouse. Nested in niri gamescope can't own the seat and
Steam falls back to a frozen XTest cursor. DM-seating also gives Steam a real login
session so its bwrap/runtime work. A steamos-session-select stub turns Steam's
"Switch to Desktop" into an exit (touch a sentinel → foxtrot-gamemode-exit.path
quits Steam + gamescope → greeter).

greetd + ReGreet + wvkbd greeter (controller-only login)

plasma-login-manager has no controller-usable OSK (touch-gated keyboard;
QT_IM_MODULE can't reach the greeter's separate PAM session). Replaced with
greetd running ReGreet inside a minimal niri compositor + wvkbd as an always-on
layer-shell OSK. ReGreet's cage default is single-window (no room for an OSK); niri
renders layer-shell so wvkbd floats above the fullscreen greeter and stays clickable
with the controller trackpad. XDG_DATA_DIRSsessionData.desktops so ReGreet lists
niri + Gaming; security.pam.services.greetd.fprintAuth = false.

Validation

Deployed to foxtrot (boot + reboot). Login confirmed live: ReGreet + OSK shown,
logged in with only the Steam Controller. Steam runs seated in the gamescope session
(no cap/bwrap breakage). Still to confirm before relying on it fully:
controller-as-mouse actually moving the cursor in-game.

Follow-ups (not blockers)

  • Lost plasma-login's kwallet/login-keyring auto-unlock — can wire pam_gnome_keyring
    into greetd if wifi PSKs start re-prompting.
  • Greeter doesn't inhibit lid-suspend yet.
  • niri runs everywhere → candidate to roll the greeter to the fleet (foxtrot proven first).

🤖 Generated with Claude Code

https://claude.ai/code/session_01XX14i2xvfiY2TrYzm3A64c

Turns foxtrot into a SteamOS-like couch-gaming machine driven entirely by the **Steam Controller** (lid closed), without compromising its primary role as a laptop. Squashes the previously-stacked #656 / #665 / #664 into one changeset, rebased onto current `main`. ### Steam migration (flatpak → `programs.steam`) The Flatpak Steam's sandbox always binds the host (niri) display sockets, so a host gamescope could never contain it — Steam escaped onto niri. nix-managed Steam embeds in gamescope properly. 332G library + native saves moved in place via btrfs rename (no re-download). ### Seated "Gaming (gamescope)" wayland-session + controller exit A session the display manager launches on its own **DRM seat**, so gamescope owns input and Steam Input drives the cursor natively (the Steam Deck model) — the only way to get controller-as-mouse. Nested in niri gamescope can't own the seat and Steam falls back to a frozen XTest cursor. DM-seating also gives Steam a real login session so its bwrap/runtime work. A `steamos-session-select` stub turns Steam's "Switch to Desktop" into an exit (touch a sentinel → `foxtrot-gamemode-exit.path` quits Steam + gamescope → greeter). ### greetd + ReGreet + wvkbd greeter (controller-only login) plasma-login-manager has no controller-usable OSK (touch-gated keyboard; `QT_IM_MODULE` can't reach the greeter's separate PAM session). Replaced with **greetd running ReGreet inside a minimal niri compositor + wvkbd** as an always-on layer-shell OSK. ReGreet's cage default is single-window (no room for an OSK); niri renders layer-shell so wvkbd floats above the fullscreen greeter and stays clickable with the controller trackpad. `XDG_DATA_DIRS`→`sessionData.desktops` so ReGreet lists niri + Gaming; `security.pam.services.greetd.fprintAuth = false`. ### Validation Deployed to foxtrot (boot + reboot). **Login confirmed live**: ReGreet + OSK shown, logged in with only the Steam Controller. Steam runs seated in the gamescope session (no cap/bwrap breakage). **Still to confirm before relying on it fully:** controller-as-mouse actually moving the cursor in-game. ### Follow-ups (not blockers) - Lost plasma-login's kwallet/login-keyring auto-unlock — can wire `pam_gnome_keyring` into greetd if wifi PSKs start re-prompting. - Greeter doesn't inhibit lid-suspend yet. - niri runs everywhere → candidate to roll the greeter to the fleet (foxtrot proven first). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01XX14i2xvfiY2TrYzm3A64c
feat(foxtrot): controller-only couch gaming — system Steam, seated gamescope session, greetd greeter
All checks were successful
/ check-format (push) Successful in 9s
/ build (push) Successful in 13m29s
9faeb0b717
Turns foxtrot into a SteamOS-like couch-gaming machine driven entirely by the
Steam Controller (lid closed), without compromising its primary role as a laptop.

Steam migration (flatpak -> programs.steam):
- The Flatpak Steam's sandbox always binds the host (niri) display sockets, so a
  host gamescope could never contain it — Steam escaped onto niri. nix-managed
  Steam embeds in gamescope properly. Library + native saves moved in place.

Seated 'Gaming (gamescope)' wayland-session + controller exit:
- A session the display manager launches on its own DRM seat, so gamescope owns
  input and Steam Input drives the cursor natively (the Steam Deck model) — the
  only way to get controller-as-mouse. Nested in niri gamescope can't own the
  seat and Steam falls back to a frozen XTest cursor. DM-seating also gives Steam
  a real login session so its bwrap/runtime work.
- steamos-session-select stub makes Steam's 'Switch to Desktop' exit: touch a
  sentinel -> foxtrot-gamemode-exit.path quits Steam + gamescope -> greeter.

greetd + ReGreet + wvkbd greeter (controller-only login):
- plasma-login-manager has no controller-usable OSK (touch-gated keyboard;
  QT_IM_MODULE can't reach the greeter's separate PAM session). Replace it with
  greetd running ReGreet inside a minimal niri compositor + wvkbd as an always-on
  layer-shell OSK. ReGreet's cage default is single-window (no room for an OSK);
  niri renders layer-shell so wvkbd floats above the fullscreen greeter and stays
  clickable with the controller trackpad (a real HID mouse at the greeter).
- XDG_DATA_DIRS -> sessionData.desktops so ReGreet lists niri and Gaming.
- security.pam.services.greetd.fprintAuth = false (greetd is the greeter PAM
  service now; password keeps the OSK path + login-keyring capture).

Validated live: ReGreet + OSK shown, logged in with only the Steam Controller.
Pending before relying on it fully: confirm controller-as-mouse in-game.
fix(foxtrot/gamemode): arm exit watcher on default.target, not graphical-session.target
All checks were successful
/ check-format (push) Successful in 1m37s
/ build (push) Successful in 1h27m15s
cffa53cc21
foxtrot-gamemode-exit.path was wanted by graphical-session.target, which niri/DMS
never starts (verified inactive even in a live niri session) — so the exit watcher
was never armed and touching the sentinel did nothing (gaming mode couldn't be
exited). default.target is active in every logged-in user session, so the sentinel
is actually watched now. Verified live: touching the sentinel fires the oneshot
(Result=success), which removes it and runs steam -shutdown + gamescope kill.
feat(foxtrot/gamemode): controller-reachable exit helper (Switch-to-Desktop is a no-op)
Some checks failed
/ check-format (push) Successful in 8s
/ build (push) Has been cancelled
872d73ca75
Verified live: system Steam's 'Switch to Desktop' never calls steamos-session-select
(empty session-select log, gamescope kept running) — same dead end as the flatpak.
The working exit is foxtrot-exit-gamemode, added once as a non-Steam library
shortcut: clicking it in Big Picture touches the sentinel -> the (now correctly
armed) foxtrot-gamemode-exit watcher quits Steam + gamescope -> greetd returns to
the ReGreet greeter. Uses a real touch binary (coreutils, not /usr/bin/touch) and
the system-Steam sentinel path. steamos-session-select stub kept as a fallback.
feat(foxtrot/greeter): dark ReGreet theme (application_prefer_dark_theme)
All checks were successful
/ check-format (push) Successful in 8s
/ build (push) Successful in 9m30s
9149ff8397
feat(foxtrot/greeter): clickable on-screen keyboard toggle
All checks were successful
/ check-format (push) Successful in 2m20s
/ build (push) Successful in 1h0m49s
25d37b6869
wvkbd can't be repositioned at runtime (anchor is compile-time), but it hides/shows
on SIGRTMIN. Add a small always-on-top waybar button (top-right, clear of the login
form and the bottom keyboard) whose on-click sends that toggle — so at the
controller/touch-only greeter you can dismiss the keyboard to see what it covers and
bring it back. Only the greeter needs it; the niri desktop has DMS's own OSK toggle.
fix(foxtrot/greeter): hold an idle inhibitor so the greeter doesn't self-suspend
Some checks failed
/ check-format (push) Has been cancelled
/ build (push) Has been cancelled
9265c5e1d4
laptop.nix sets logind IdleAction=suspend after IdleActionSec=11m. Left at the
greetd/ReGreet login screen (no user session, nothing holding the machine awake),
foxtrot suspended itself after 11 minutes and dropped off LAN+tailnet — which read
as the VITURE link flapping but was really idle-suspend. The greeter now spawns a
block-mode idle:sleep systemd-inhibit for its lifetime, released when a session
starts. Lid-close still suspends by design (LidSwitchIgnoreInhibited defaults on).
Verified live: inhibitor registered (systemd-inhibit --list shows greeter sleep:idle block).
feat(foxtrot/greeter): start the OSK hidden, show via the toggle button
Some checks failed
/ check-format (push) Has been cancelled
/ build (push) Has been cancelled
6bbe7ff560
feat(foxtrot/gaming): media/volume/brightness keys in the gamescope session
All checks were successful
/ check-format (push) Successful in 7s
/ build (push) Successful in 6m45s
aa8825aed4
gamescope passes XF86Audio*/XF86MonBrightness* keys straight to Steam (which
ignores them) and the seated gaming session has no desktop shell to bind them, so
the laptop's function keys were dead in-game. Run triggerhappy for the gaming
session's lifetime (backgrounded in the session script, trap-killed on exit) mapping
volume->wpctl, brightness->brightnessctl, media->playerctl. Scoped to the gaming
session so it doesn't double-fire with DMS on the niri desktop. daniel is in input
(evdev read) + video (backlight write); wpctl/playerctl use the session's
PipeWire/bus. For the lid-open 'regular laptop' gaming case where the keyboard's in hand.
fix(foxtrot/gaming): filter thd to keyboard devices + inhibit idle in-game
All checks were successful
/ check-format (push) Successful in 7s
/ build (push) Successful in 5m51s
afc4bb31b3
Two fixes after live-testing the media/brightness keys:

1. thd got no key events when handed the raw /dev/input/event* glob: the
   controller's motion-sensor device reports "not suitable" and breaks thd's
   reads. Select only keyboard-class devices via udev (ID_INPUT_KEY=1 and not
   ID_INPUT_JOYSTICK) — the AT keyboard (volume/mute) + Framework Consumer Control
   (brightness). Confirmed live: volume + brightness now respond in-game.
   (The Framework emits both a media code and an F-key code per top-row press;
   thd matches the media code.)

2. The gaming session held no idle inhibitor, so logind IdleAction=suspend (11m,
   laptop.nix) suspended foxtrot mid-game / dropped it off the network — which
   repeatedly derailed testing. Wrap gamescope in systemd-inhibit idle:sleep for
   its lifetime, like the greeter.
All checks were successful
/ check-format (push) Successful in 7s
Required
Details
/ build (push) Successful in 5m51s
Required
Details
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin foxtrot-couch-gaming:foxtrot-couch-gaming
git switch foxtrot-couch-gaming
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lytedev/nix!666
No description provided.