Files
steamify-cachyos/AGENTS.md
T
theupriser 948dc1f587 feat: "Gaming on NVIDIA" replaces the SteamOS conversion on NVIDIA PCs
gamescope's gaming mode shows a corrupted picture on NVIDIA (NVIDIA bug 5240452), also nested in KDE and in a KWin-only
session. With an NVIDIA GPU the conversion (gaming, boot, single, glyphs) is hidden unless already on, and nvidia takes its
place: Steam installed when missing and started at login on the Plasma desktop (user unit), with the sub-option bigpicture
for Big Picture or the normal window. The kernel parameters and early NVIDIA modules (RTX 20 series or newer) are kept as
part of it. tests/nvidia-test.sh covers the component and the menu rules against a fake GPU.
2026-10-01 19:04:58 +02:00

384 lines
25 KiB
Markdown

# AGENTS.md
Guidance for AI coding agents working on this repository. See `README.md`
for what the script does from a user's point of view.
## Project overview
**Steamify CachyOS**: a bash wizard that makes CachyOS (KDE Plasma 6 + `plasma-login-manager`)
boot into a SteamOS-style gamescope session, with working switching between
gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
(DMI `Valve`/`Fremont`) running CachyOS Desktop edition.
## Layout
- `steamify.sh` - the only entry point. Pre-flight checks,
sources `lib/*.sh`, runs the menu and applies the plan. No component
logic here.
- `patches/` - kernel module sources and patches the scripts build or
apply, never inline in the shell code. Read them with `patch_file
<name>` (`lib/common.sh`, from the checkout); the bundle embeds every file
in `patches/` and overrides `patch_file`. Add new ones to its README.
- `services/` - the systemd units the scripts install (`.service`,
`.timer`, `.path`), never inline in the shell code. Read them with
`service_file <name> [KEY=value...]` (`lib/common.sh`), which fills in
`@KEY@` placeholders; the bundle embeds every file in `services/` too
(`service_raw`). Add new ones to its README. Small drop-ins for other
packages' units stay inline.
- `ui/` - the app: `steamify-ui` (PySide6; runs `steamify.sh --backend`,
`lib/backend.sh`) and `ui/qml/`: `Main.qml` (window, header, which screen
shows), `AppState.qml` (all state and logic, input actions), one
`*Screen.qml` per screen, small widgets (`Btn`, `Chip`, `Badge`, ...), and
the singletons `Theme` (colours, fonts), `Texts` (item texts) and `Input`
(controller/keyboard/remote and its button names), listed in `qmldir`.
Screens get the `AppState` as `app` and only show it or call its functions.
- **Paths.** Everything in the home folder lives under `steamify`:
`$STATE_DIR` (`~/.local/state/steamify`), `$STEAMIFY_DATA`/`$STEAMIFY_BIN`
(`~/.local/share/steamify/{app,bin}`), `lib/state.sh`. Never add files
under the old name `cachyos-gamescope-boot`; `migrate_layout` moves those
of older versions (symlinks keep older releases working). Keep the
`.bak-gamescope-wizard` backup suffix: existing backups are found by it.
- `lib/*.sh` - one file per responsibility, each defining functions only
(no top-level side effects besides constants). See the table in
`TECHNICAL.md`.
- **Components.** Every menu item `<id>` (listed in `COMPONENTS` and `LABEL`
in `lib/menu.sh`) provides `<id>_status` (return 0 if on, detected from
the system, no state file needed), `<id>_enable` and `<id>_disable`, all
idempotent. `<id>_enable` is also used to re-apply. Optional
`<id>_available` is checked via `component_available` (e.g. `machine`
only on Fremont). Turn-on order is `COMPONENTS` order, turn-off reverse;
`gaming` must stay first. Dependencies live in `toggle_component`.
- **NVIDIA** (`lib/nvidia.sh`): gamescope's session is broken on NVIDIA, so the SteamOS conversion (`gaming`, `boot`, `single`,
`glyphs`) is hidden there (unless already on) and `nvidia` + its sub-option `bigpicture` ("Gaming on NVIDIA") replace it:
Steam on the Plasma desktop, started at login. Never reintroduce a gamescope session for NVIDIA without re-testing on the
hardware (notes: steamify-cachyos-dev, `nvidia/`). It can't be tested without the hardware: keep the branch behind
`.no-release-yet` until it was.
- **Feature versions.** Set `FEATURE_VERSION[<id>]` (`lib/menu.sh`) to the new
`VERSION` whenever what `<id>_enable` sets up changes (a new component gets
one too): installs recorded with an older version
are ticked and re-applied ("update" in the plan and the app). New default
options are ticked for installs whose parent (top-level: `gaming`) is on
(`feature_new`, shown as "new").
Options shown but left unticked in a confirmed run are recorded as
`off` (`feature_record_unticked`), or `feature_new` would tick them again.
Status still comes from the system; don't add ad-hoc `<id>_repair` checks
for new changes.
- **Reversibility.** Every per-user KDE setting a component changes goes
through `kset <component> <file> <group|group> <key> <value>`
(`lib/state.sh`), which records the old value once; `<id>_disable` calls
`krevert <component>`. Never call `kwriteconfig6` directly for settings a
component owns. System files are backed up with `backup_file` and restored
on disable.
- **Single-file build.** `.github/tools/bundle.sh` inlines `lib/*.sh` in the order of
the entry point's `for lib in ...; do` source loop and wraps everything
after that loop in `main()`. Keep that loop on one line, keep all logic in
functions, and don't rely on `SCRIPT_DIR` for anything but sourcing.
The workflow is one file for GitHub and a Gitea mirror (`github.server_url`): build and check run on both
(shellcheck through `ludeeus/action-shellcheck`, which brings its own binary; the artifact with
`upload-artifact@v4` on GitHub and `christopherhx/gitea-upload-artifact@v4` elsewhere: never mention
`upload-artifact@v3`, GitHub fails a workflow for a deprecated version even in a skipped step); the
release is `gh` on GitHub and `akkuman/gitea-release-action` on Gitea. A new release on GitHub also starts the
Steamify ISO's release (`gh workflow run iso-1-github-tag.yml` in steamify-cachyos-live-iso, secret
`ISO_DISPATCH_TOKEN`; skipped without it): the ISO repo's GitHub tag names it, the Gitea mirror builds it.
CI (`.github/workflows/bundle.yml`) publishes the bundle as release
`v$VERSION` on pushes to `main`; an existing version is never overwritten,
so bump `VERSION` for every release.
## Conventions
- Bash, `set -uo pipefail` (no `-e`): check exit codes of steps that matter
explicitly (`|| { err ...; exit 1; }` or `|| return 1`).
- Use the helpers from `lib/common.sh` (`info`, `ok`, `warn`, `err`,
`ask_yn`, `backup_file`) for all output and prompts.
- Runs as the normal user; use `sudo` per command, never require root.
Per-user files go in `$HOME` of the invoking user (the script refuses to
configure another user).
- **Idempotent**: every step must be safe to re-run without duplicating
lines or settings. Back up system files with `backup_file` before the
first edit.
- Don't overwrite package-owned files in `/etc/xdg` or `/usr`. KDE settings
go into the user's config via `kwriteconfig6` (or `merge_kde_config` for
whole Valve ini files).
- Package installs after a user already confirmed use `--noconfirm`: a
second pacman prompt consumes the next scripted answer.
- Comments explain *why* (the CachyOS/Plasma quirk being worked around),
not what the next line does.
- **Versioning and releases.** SemVer in `VERSION` (`steamify.sh`,
shown in the menu header and the bundle header).
- Every commit gets an entry in `CHANGELOG.md` under its version (short hash
+ subject; a commit can't contain its own hash, so fill it in with the
next commit). The section heading must be `## <VERSION> - <date>`: CI
cuts the release notes out of the changelog by that heading.
- **Branches: one release branch per version.** Work for a release goes
into `release/X.Y.Z` (e.g. `release/2.7.0`), branched from `main` when
that version starts. That branch bumps `VERSION` to `X.Y.Z` and adds the
`## X.Y.Z - <date>` changelog section (first commit on it).
- Every change gets its own branch from the release branch:
`feature/<name>` (new behaviour) or `bugfix/<name>` (a fix), and a
pull request **into the release branch**, not into `main`. Its commits
add their changelog lines to that version's section.
- When the release is done and tested (the test plan in
steamify-cachyos-dev, `TESTPLAN.md`), the release branch gets a pull
request **into `main`**. Merging it publishes the release (below).
- A fix for a released version: a new `release/X.Y.Z+1` from `main`,
with the `bugfix/` branch into it; don't reopen an old release branch.
- Up to the release branch, the agent manages it: create the
`feature/`/`bugfix/` branches, commit, push, and merge them into the
release branch itself once tested (with `gh`: `gh pr create --base
release/X.Y.Z` + `gh pr merge --merge`; no review needed). Without
`gh` (`command -v gh`), ask the user to install it (Ubuntu:
`sudo apt install gh`, then `gh auth login`) instead of working around
it. Only the
release branch's pull request into `main` is the user's: never commit
or merge to `main`, the user merges that one. Never work directly on
`main` or a `release/*` branch either: edits, even uncommitted ones,
go on a `feature/`/`bugfix/` branch (create it before editing; the
release branch only gets the version-bump commit and merged PRs).
Don't push a new `release/*` branch before it's needed: a push can
start CI builds (a dev ISO).
A machine without a git identity: pass the repo's author per command
(`git -c user.name=... -c user.email=... commit`, taken from `git
log`) instead of changing the git config.
A `.no-release-yet` file at the root of a feature/bugfix branch means
it must not be merged into the release branch yet (work in progress,
untested on hardware). When the feature is complete, ask the user
whether it may be released; on a yes, delete the file, then open the
pull request and merge it.
Keep a feature/bugfix branch up to date by merging (or rebasing on)
its release branch, the release branch by merging `main` when that
moved.
- A push to `main` publishes release `v$VERSION` (tag + bundle + that
changelog section) and marks it latest. An existing version is never
overwritten: without a bump nothing is released, and pull requests show
a warning when `VERSION` is a version that's already released (so a
release branch that forgot its bump shows it on every PR).
- A release that adds, removes or renames a menu row also retakes the
README screenshot (`assets/screenshot-menu.png`): the app from the
branch on a Steam Machine, every row visible (window 1280 wide, tall
enough), the header showing the new `VERSION`, scaled to 1600 px wide.
- Users install through `releases/latest/download/steamify.sh`
(GitHub's newest release). The `latest` tag and release follow the newest
version tag too (moved, asset replaced, when a new version is released),
so the older URL `releases/download/latest/...` keeps working.
## Non-obvious behaviour to preserve
- `apply_changes` sets `$LOGIN_MANAGER` from the menu: `sddm` when single
user mode is wanted, else `plasmalogin`. Toggling single user re-applies
`gaming` so it moves to the other login manager. Single user mode hides
the launcher's Session dropdown via kickoff `primaryActions=3` (restricting
`action/logout` also hides Restart/Shut Down). Two login-manager paths:
**sddm** (like SteamOS: `steam-set-session` writes
`/etc/sddm.conf.d/zz-steamos-autologin.conf`, which SDDM honours; we add
`User=`/`Relogin=` in `10-gamescope-autologin.conf`, and `/etc/sddm.conf`
must not contain `[Autologin]` since it's read last) and **plasmalogin**
(needs the sync bridge and the shortcut's sudoers rule). Both must keep
working.
- `steam-set-session` only writes `/etc/plasmalogin.conf.d/zz-steamos-autologin.conf`;
the base `/etc/plasmalogin.conf` wins, hence the sync bridge. The sync
service needs `StartLimitIntervalSec=0`, or bursts of session switches
get it rate-limited and all later switches silently stop working.
- The sync script must only edit `Session=` inside `[Autologin]`.
- The Steam desktop autostart unit is guarded with
`ExecCondition=... XDG_CURRENT_DESKTOP = KDE`, so it doesn't start a
second Steam inside gamescope.
- Vapor theme: comes from the `cachyos-vapor` package (system-wide, in
`/usr/share`); only remove it on disable if we installed it. It is applied
with `lookandfeeltool --resetLayout` ("Desktop and window layout"), which
replaces the panel/desktop layout: the layout files are backed up to
`$STATE_DIR/theme-layout` and restored, and every key from Vapor's
`contents/defaults` goes through `kset` first. `plasma-apply-colorscheme`
runs with the `ColorScheme` key cleared, since it skips a scheme already
named there. The layout reset drops single user's launcher settings, so
`single_launcher` is re-applied.
- SteamOS extras (`lib/steamos-extras.sh`, part of the theme): downloaded from
Valve's newest `steamdeck-kde-presets` (repo db gives name + SHA-256) into
`/usr/local`, never `/usr`. `gaming-return.svg` is a symlink in the package:
install `steam-gaming-return.svg` under that name. The shortcut's icon is
switched with `set_shortcut_icon`, since the conversion is created before
the theme. An existing KWallet is never replaced or removed.
- Plasma 6 has no separate systemtray containment: tray settings live on
the systemtray applet itself (Valve's setup script targets Plasma 5).
- Restart plasmashell via `systemctl --user` (`plasma-plasmashell.service`),
not `plasmashell --replace` from the script's shell: a shell without the
session environment yields a light-themed desktop (context menus, apps it
launches).
- Live Plasma changes (`qdbus6 ... evaluateScript`, `lookandfeeltool`) only
run when `plasmashell` is running; config-file fallbacks cover the rest.
- plasmashell writes its in-memory config back on exit: edit panel/applet/
wallpaper files only between `stop_plasmashell_for_edit` and
`restart_plasmashell_if_stopped` (`lib/common.sh`).
- LED driver: `leds-valve-dkms-git`'s Makefile builds against `uname -r`,
not DKMS's target kernel: `/etc/dkms/leds-valve-dkms.conf` sets
`MAKE[0]="make KVERSION=${kernelver}"` (written before the AUR install;
someone's own override there is backed up, kept with ours appended, and
restored on disable).
Without it, other kernels build against the running kernel's tree and
fail (CachyOS kernels are clang-built; DKMS adds `LLVM=1` only for the
target's tree). Headers for every installed kernel are installed first,
and `ensure-kernel-headers.service` installs missing ones at boot (a
pacman hook can't run pacman), which triggers DKMS's install hook. The module creates
`/sys/class/leds/valve-leds*`.
- `boot` ("Boot into: [gamescope] / desktop") is a choice row, not a
checkbox: on = desktop. It's a sub-option of the conversion: `menu_visible`
only shows it (indented, `└`) while `gaming` is ticked, so menu numbers
after it shift by one when the conversion is unticked. It needs `gaming` (ticking it ticks gaming,
unticking gaming unticks it) and is never preselected (`NO_PRESELECT`), so
the conversion boots into gamescope by default. Desktop is
`steamify-boot-desktop.service`, ordered `Before=` the login managers:
`steam-set-session plasma.desktop` plus the plasmalogin sync bridge when it
exists, at every boot. CachyOS's `cachyos-gamescope-autologin` still sets
gamescope during each desktop session; the unit corrects it at boot.
- `bios` is an *action* (`ACTIONS` in `lib/menu.sh`) and a sub-option of `machine`, not an on/off
component: never preselected (not even on a first run), never re-applied
by `a`, not listed in the state overview, and `bios_status` is always off.
Only selectable when Valve's version differs from the installed one
(`component_selectable`, greyed out otherwise). Download, SHA-256 and the
fwupd device check (`get-details --json`: no `UpdateError`) come before the
warnings. The warning box uses `█` for its frame: Konsole draws a long
coloured row of `#` narrower, so the right edge wouldn't line up.
Keep both confirmations (y/N, then typing `UPDATE`) and the warnings; the
firmware comes from the newest `holo-X.Y` repo (`.files` db names the
`.cab`, `.db` gives the SHA-256), and fwupd itself refuses non-Fremont
hardware. In the VM, test it with `WIZARD_BIOS_DRY_RUN=1` (skips only the
device check, never flashes) and a faked version (dev-env
`BIOS_VERSION=F7F0107 ./run.sh --fremont`).
- The entry point loops: menu, run, "back to the menu" (or `[m]`/`[r]` when a
restart is needed), until `q`. When a restart is needed, `q` asks "Restart
now? [Y/n]" (`quit_prompt`): `n` goes back to the menu, the next `q` asks
again. Scripted input that runs out quits without restarting (never
restart on EOF: `ask_yn` would take its default).
- Steamify shortcut (`launcher`): "Steamify CachyOS" (desktop and launcher,
id `steamify-ui.desktop`, the app's own id) runs `curl | bash` of
`releases/latest/download/steamify-app.sh` without a terminal (in Konsole
only while PySide6 is missing, for sudo); "Steamify Terminal" (launcher)
runs `steamify.sh` in Konsole. Both always the newest release. The entry
has `X-Steamify-Shortcut=true`: steamify-app.sh doesn't overwrite it, and
disable only removes it then. A pre-2.0.1 shortcut (terminal only) is
ticked by `launcher_repair`. Its icon, `assets/steam-gaming-settings.svg`
(Valve's GPL-2.0 return icon with a gear), is a release asset too, and is
downloaded from there (Steam's icon if that fails). Desktop files are
written with their mode already set (`install_executable`): Plasma opens a
desktop icon it first saw non-executable in an editor.
The start script closes its window after a 10-second countdown on success
and waits for Enter after an error; it uses `pipefail`, or a failed
download would run an empty script and count as success.
- HDMI refresh boost (`hdmi`, `lib/hdmi-refresh.sh`): retired in 2.2.0
(it needed the pinned kernel); only removal is left (see the power-off
fix below). `hdmi_enable` refuses. debugfs is root-only (glob it under
sudo), and `edid_override` takes exactly `reset` with no newline.
- Steam Machine CEC driver (`cec_driver_enable`, `lib/cec.sh`): mainline
`cros_ec_cec` lacks Fremont, so HDMI-CEC builds Valve's copy (evlaV
`linux-integration`, pinned commit + SHA-256) with DKMS for every kernel.
It's patched to register its notifier without a port name when the board
has one CEC port: amdgpu registers its HDMI notifier nameless, and the
kernel only pairs that with a named lookup ("Port C") when the CEC driver
registered first, which never happens since amdgpu loads from the
initramfs. Without it `/dev/cec0` exists but stays at `f.f.f.f`.
The source comes from one raw.githubusercontent.com download, which GitHub rate-limits (HTTP 429 after a
day of test installs, per address): it's kept in `/var/cache/steamify/cros-ec-cec-<sha256:12>.c` and reused
while its checksum matches. The name carries the checksum, so a new pin (commit + SHA-256 in `lib/cec.sh`)
downloads its own file once and removes the old one. The repo is an unofficial mirror of Valve's kernel
without releases: if it ever disappears, ship the file with Steamify. The test VMs share the host's cache.
- Power-off fix (`poweroff`, `lib/fremont-poweroff.sh`, a default sub-option
of `machine`): recent kernels (7.2 and the 6.x/7.0/7.1 updates with the
backport) keep the firmware's S4/S5 wake bit on GPIO
pin 18, so the Steam Machine boots again right after powering off
(Valve's kernel clears it at probe, not for upstream). The DKMS module
`steamify-fremont-poweroff` (`patches/steamify-fremont-poweroff.c`) clears
it in a power-off-prepare handler, built for each kernel. It made the kernel pin
unnecessary: `kpin` is only available while on (`kpin_available`), and
`detect_components` unticks it, so a normal run removes an existing pin.
`hdmi` (it needed the pin) is retired the same way: `hdmi_status` is on
while anything of it is left (saved displays, hotplug unit, EDID files,
old kernel parameter), `hdmi_available` only then, it's always unticked,
and `hdmi_disable` removes all of it. CachyOS's
`linux-cachyos` is clang-built, `-bore` GCC-built: let DKMS pick the
compiler, never pass `LLVM=1`. Test shutdown on the real machine for every
new major kernel.
- Add as non-Steam game (`steamgame`, `lib/steam-game.sh`, sub-option of
`launcher`, ticked along with it, 2.5.1): edits Steam's `shortcuts.vdf`
with `patches/steam-shortcuts.py` only while Steam is closed (Steam
rewrites it on exit), then starts Steam again. Never close Steam in gaming
mode or when `SteamGameId` is set (the run is a Steam game). An existing
entry for the start script is updated, not doubled; removal takes every
entry for it. Controller input through Steam's desktop layout arrives as
keys: Enter must select (A), Esc must never quit (B); the app re-execs
without Steam's overlay preload.
- Update notifications (`notify`, `lib/update-notifier.sh`, top-level,
ticked by default, 2.5.0): a user timer (daily, and the service is wanted
by `plasma-workspace.target`) runs `patches/steamify-notifier.py`, which
compares GitHub's newest release with the `seen` version every run records
(`notify_seen` in the entry point, only while it's on, with `frontend`: app or terminal, which Open Steamify starts again, in a `systemd-run --scope` since the check's own unit is stopped when it exits). It must never
update anything itself: notification + tray icon, Open Steamify / Skip
this version (`skipped`). Desktop only (exits without `plasmashell`).
- VRAM booster (`vram`, `lib/vram-booster.sh`, top-level, ticked by
default, 2.3.0; only available when `/sys/fs/cgroup/dmem.capacity` lists a
`vram`/`vidmem` region (numbered too) of at least 2 GB, whatever the brand;
greyed out with an NVIDIA card whose driver lists none, `vram_selectable`,
faked with `WIZARD_VRAM_FAKE_NVIDIA=1`; `WIZARD_VRAM_CAPACITY=<file>` reads
the regions from a copy): CachyOS's `dmemcg-booster` (system + user service) and
`plasma-foreground-booster`, like SteamOS 3.9's VRAM management. The
latter only starts with `kcgroupsrc [Foreground Booster] autostart=true`
(set with `kset`). Disable removes only the packages it installed
(`state_get vram installed_pkgs`).
- Install-time mode (`steamify.sh --defaults`, 2.6.0): applies what the menu
would preselect, no menu or prompts (needs passwordless sudo). The Steam
Machine ISO ([steamify-cachyos-live-iso](https://github.com/theupriser/steamify-cachyos-live-iso))
runs it in the installer for a user who has never logged in: no session
bus, user systemd or plasmashell. So every `systemctl --user` goes through
`user_systemctl` (`lib/common.sh`; without a session only unit files change,
via `--root=/`), and the theme, when the layout is still `/etc/skel`'s,
removes it so Plasma builds Vapor's layout at the first login. What needs
that layout (single user's launcher) runs then, from a one-time autostart
(`lib/first-login.sh`, `--first-login`) that also opens the app.
`--options <id>,...` (exactly these on, the rest off; an item on brings its
parent) and `--boot gamescope|desktop` (desktop only with `gaming`) (2.7.0,
`defaults_options` in `lib/menu.sh`); the ISO's installer page passes them.
Items this PC can't use are left off with a warning, not an error.
`steamify.sh --boot gamescope|desktop` on its own (2.7.0, for scripts) only
switches `boot` on an installed conversion: `WANTED` is `CURRENT` plus
that, so no updates or removals a normal run would pick.
- What Steam's System settings show (SteamOS conversion, 2.9.0): the OS name
stays CachyOS's everywhere (legal clarity; Steam's OS Name is
`lsb_release -d`, not touched). `/etc/os-release` gets `VERSION_ID=steamos-X.Y`
(Steam's OS Version: Valve's newest jupiter repo; offline, the previous
value), `VERSION_CODENAME=steam-machine`, `VARIANT="Steamify <version>"` and
`VARIANT_ID=steamify-<version>` (Steam shows VARIANT_ID as OS Variant),
refreshed by `os_version_refresh` after every run that changes something; a
pacman hook (`zz-steamify-os-release`) runs after cachyos-hooks. KDE's About
this System would show VERSION_ID after the name: `kcm-about-distrorc`
`UseOSReleaseVersion=true` (kset) makes it show os-release's VERSION, which
CachyOS doesn't have. Never `NAME` or `PRETTY_NAME`: limine-snapper-sync
finds the boot entries by the OS name ("Target OS name ... not found in
/boot/limine.conf"); `/etc/default/limine` gets `TARGET_OS_NAME="CachyOS"`
when it has none, as a guard. The serial number (tmpfiles rule) is Steam
Machine support's, so Fremont only.
- `Relogin=true` means a gamescope that fails to start is relaunched in a
tight loop; keep that in mind when changing session handling.
## Checking changes
The bundle puts every module in one file, so shellcheck sees all their
`local` variables together: don't reuse a name another module uses as an
array (e.g. `g` in `lib/state.sh`), or CI's shellcheck on the bundle fails.
There is no test suite. At minimum:
```bash
for f in steamify.sh lib/*.sh; do bash -n "$f"; done
shellcheck -S warning steamify.sh lib/*.sh # if available
```
Behaviour is verified in a CachyOS QEMU/KVM test VM. The VM scripts and a
Claude Code skill describing the whole test workflow (snapshots, SSH, running
the wizard with scripted menu input such as `printf '2\n\ny\nn\n'`, the
per-component checks; when stdin is not a terminal the menu falls back to a
numbered prompt, which is what scripted runs use, reboot checks, the full test matrix, `--fremont` to
fake Steam Machine hardware) live in
[steamify-cachyos-dev](https://github.com/theupriser/steamify-cachyos-dev).
Gamescope itself and the real LED bar can only be verified on hardware.