# 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 ` (`lib/common.sh`, from the checkout); the bundle embeds every file in `patches/` and overrides `patch_file`. Add new ones to its README. - `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. - `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 `` (listed in `COMPONENTS` and `LABEL` in `lib/menu.sh`) provides `_status` (return 0 if on, detected from the system, no state file needed), `_enable` and `_disable`, all idempotent. `_enable` is also used to re-apply. Optional `_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`. - **Feature versions.** Set `FEATURE_VERSION[]` (`lib/menu.sh`) to the new `VERSION` whenever what `_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"). Status still comes from the system; don't add ad-hoc `_repair` checks for new changes. - **Reversibility.** Every per-user KDE setting a component changes goes through `kset ` (`lib/state.sh`), which records the old value once; `_disable` calls `krevert `. 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. 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 `## - `: CI cuts the release notes out of the changelog by that heading. - Every PR that should be released bumps `VERSION` and adds its section. 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. - 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). 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`. - 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. - 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 region of at least 2 GB; greyed out with an NVIDIA card, `vram_selectable`, faked with `WIZARD_VRAM_FAKE_NVIDIA=1`): 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`). - `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.