Compare commits

...
226 Commits
Author SHA1 Message Date
theupriser f05078d932 docs: changelog hashes for 2.11.0, and the tests move
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 14:37:42 +02:00
theupriser 4d387b2f86 chore: the tests live in steamify-cachyos-dev, remove tests/ 2026-10-02 14:37:02 +02:00
theupriser 19089a1eda Merge pull request #74 from theupriser/feature/silent-steam-start
Sync git.upriser.nl mirror / sync (push) Skipped
Single-file build / bundle (push) Successful in 12s
feat: Start Steam silently in desktop mode (opt-in), Steam Deck icons under the conversion, installer fixes
2026-10-02 14:34:56 +02:00
theupriser 2f7b53f64b fix: the Steam Machine serial number step is not fatal in the installer chroot (read-only /sys); the tmpfiles.d file applies at boot
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 12:22:20 +02:00
theupriser bd8f387cf5 fix: STEAMIFY_NO_DOWNLOAD finds the bundle through BASH_SOURCE (a bundle has no SCRIPT_DIR: the install failed with an unbound variable)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 11:38:25 +02:00
theupriser 45a23aff34 feat: STEAMIFY_NO_DOWNLOAD: the first login runs the ISO's Steamify bundle instead of the newest release
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 11:11:18 +02:00
theupriser f3810a9fdd fix: the SteamOS theme drops the blank Discover pin from the taskbar (also on the first login of an ISO install)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 10:52:04 +02:00
theupriser 188799e951 fix: the SteamOS conversion no longer lists the silent start in what it changes (it is its own opt-in option)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 10:43:25 +02:00
theupriser 079e725ba8 feat: "Start Steam silently in desktop mode" is opt-in (it was always on); an existing systemd unit counts as on and is migrated by the update
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 10:16:40 +02:00
theupriser 7869b472be feat: Steam Deck/Machine icons is a sub-option of the SteamOS conversion (it only changes gaming mode)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 10:15:59 +02:00
theupriser fbdad5a785 feat: the silent option is Steam's own autostart entry (~/.config/autostart/steam.desktop with -silent), so its "Run Steam when my computer starts" follows it; the systemd unit is gone
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 09:28:03 +02:00
theupriser 349492423e feat: "Start Steam silently in desktop mode" is always shown, also on a Steam Machine
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-02 09:10:26 +02:00
theupriser 613149a807 docs: changelog for the silent start option
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 23:04:18 +02:00
theupriser 2e51d1d3f3 feat: "Start Steam silently" sub-option for the SteamOS conversion (silent) and Gaming on NVIDIA (nvsilent) 2026-10-01 23:04:18 +02:00
theupriser e5110ada40 chore: bump VERSION to 2.11.0 2026-10-01 23:02:37 +02:00
theupriser 2c004322af Merge pull request #72 from theupriser/bugfix/changelog-2.10.0-trim
Sync git.upriser.nl mirror / sync (push) Skipped
Single-file build / bundle (push) Successful in 24s
docs: trim the 2.10.0 changelog
2026-10-01 21:54:20 +02:00
theupriser 3915e1a96f docs: trim the 2.10.0 changelog to the user-facing changes 2026-10-01 21:54:13 +02:00
theupriser 098dc3d0d8 chore: remove .no-release-yet, 2.10.0 is ready 2026-10-01 21:54:11 +02:00
theupriser 48352b5601 Merge bugfix/nvidia-shellcheck into release/2.10.0
Sync git.upriser.nl mirror / sync (push) Skipped
Single-file build / bundle (push) Successful in 11s
2026-10-01 21:43:21 +02:00
theupriser 05b8f11c19 docs: changelog hashes for 2.10.0 2026-10-01 21:43:21 +02:00
theupriser 8b96e2d8a9 fix: shellcheck of the bundle: nvidia_enable's local args clashed with an array of another module (renamed unit_args) 2026-10-01 21:43:21 +02:00
theupriser f1fb18ec81 docs: changelog for 2.10.0, one entry per commit with its hash 2026-10-01 21:41:02 +02:00
theupriser 1987fa903b Merge feature/extended-controller-support into release/2.10.0 2026-10-01 21:29:42 +02:00
theupriser b7dc931a4f Merge feature/nvidia-gaming-fix into release/2.10.0
# Conflicts:
#	CHANGELOG.md
2026-10-01 21:29:42 +02:00
theupriser 3d20a7edb0 chore: Version 2.10.0 2026-10-01 21:29:34 +02:00
theupriser f1983a9caf docs: README screenshots for the Steam Machine and for NVIDIA PCs, side by side 2026-10-01 21:24:36 +02:00
theupriser 01a77b7dc7 feat: new opt-in options show the new badge without being ticked 2026-10-01 21:11:51 +02:00
theupriser 83248a8478 feat: extended controller support: Xbox wireless dongle (xone) and Xbox Bluetooth (xpadneo) 2026-10-01 21:11:51 +02:00
theupriser 5811f5c8a4 fix: Add as non-Steam game isn't offered on NVIDIA PCs (no gaming mode there)
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 21:11:46 +02:00
theupriser 71bb2e7aca chore: Version 2.10.0 2026-10-01 21:05:05 +02:00
theupriser d0e4d8cf79 feat: Big Picture on NVIDIA starts on an empty Plasma session, so restored windows don't cover it 2026-10-01 19:19:51 +02:00
theupriser d1373e28a6 feat: single user mode on NVIDIA PCs: SDDM autologin into the Plasma session, without the conversion 2026-10-01 19:13:46 +02:00
theupriser d6db4f6f2d 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
theupriser a5e9015d93 fix: NVIDIA fix handles Limine's += command line, underscore spelling; hardware test sets SCRIPT_DIR 2026-10-01 17:19:23 +02:00
theupriser ae238ff383 chore: .no-release-yet: this NVIDIA branch is not ready to be released
Sync git.upriser.nl mirror / sync (push) Skipped
2026-10-01 15:40:02 +02:00
theupriser c1217bcea3 feat: NVIDIA fix only for RTX 20 series or newer, with the VRAM booster's chwd legacy check (vram_nvidia_legacy_id) 2026-10-01 15:20:02 +02:00
theupriser f3d67fbe52 fix: never install an empty NVIDIA hook or script (a failed patch_file piped into install succeeded with no input) 2026-10-01 15:07:16 +02:00
theupriser 84cf0959e4 feat: NVIDIA modules in the initramfs only while every kernel has them, decided again by a pacman hook at every kernel change 2026-10-01 14:42:06 +02:00
theupriser 7caefb7da6 test: nvidia-hardware-test.sh to check, apply and visually verify the NVIDIA fix on a real PC 2026-10-01 13:20:01 +02:00
theupriser 453656a0a2 fix: NVIDIA fix checks the edit and the generated boot entries for every boot loader (limine.conf, systemd-boot entries, grub.cfg) 2026-10-01 13:07:07 +02:00
theupriser 6aa1377e16 fix: NVIDIA fix on Limine edits the existing cmdline line; early modules only when every kernel has them
Found by running it in a VM: the appended += line became literal text on the
kernel command line, and a kernel without the modules made limine-mkinitcpio
skip its boot entry.
2026-10-01 12:48:56 +02:00
theupriser 6bcae8af18 test: NVIDIA fix against a fake RTX 5080, stub modinfo and temp boot loader files 2026-10-01 11:07:32 +02:00
theupriser 3cbeb58211 docs: AGENTS.md: never work on main or a release branch, hardware-specific fixes, git identity 2026-10-01 10:54:49 +02:00
theupriser 83561bb214 feat: NVIDIA fix for gaming mode (kernel parameters and early modules)
Untested on hardware; .no-release-yet keeps it out of the release.
2026-10-01 10:54:27 +02:00
theupriser ca027cb607 chore: Version 2.9.7 2026-10-01 10:54:16 +02:00
theupriser d93f9ea56c Merge pull request #71 from theupriser/release/2.9.6
Single-file build / bundle (push) Successful in 12s
Sync git.upriser.nl mirror / sync (push) Skipped
Release/2.9.6
2026-09-30 13:34:33 +02:00
theupriser 761f921e40 Merge pull request #70 from theupriser/bugfix/iso-steamify-ref
Sync git.upriser.nl mirror / sync (push) Skipped
Single-file build / bundle (push) Successful in 13s
fix(ci): a release branch builds its dev ISO with its own Steamify
2026-09-30 12:46:20 +02:00
theupriser 7a54a2fa59 fix(ci): a release branch builds its dev ISO with its own Steamify (steamify_ref), not the newest release 2026-09-30 12:45:14 +02:00
theupriser 9eab3978a9 docs: changelog hashes for 2.9.6
Sync git.upriser.nl mirror / sync (push) Skipped
Single-file build / bundle (push) Successful in 11s
2026-09-30 12:33:28 +02:00
theupriser 9602d9d00b Merge pull request #69 from theupriser/bugfix/iso-dispatch-ref
Sync git.upriser.nl mirror / sync (push) Skipped
Single-file build / bundle (push) Successful in 14s
fix(ci): main starts a real ISO release, a release branch a dev ISO
2026-09-30 11:41:40 +02:00
theupriser 9a2965d339 fix(ci): the ISO workflow always runs from master; main asks for a release ISO, a release branch for a dev ISO
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 11:32:36 +02:00
theupriser c42d5454bf fix(ci): a version published from main starts a real ISO release, a release branch a dev ISO 2026-09-30 11:31:37 +02:00
theupriser b3bed43854 chore: Version 2.9.6 2026-09-30 11:31:25 +02:00
theupriser 3e79451244 Merge pull request #68 from theupriser/release/2.9.5
Single-file build / bundle (push) Successful in 12s
Sync git.upriser.nl mirror / sync (push) Skipped
chore: Version 2.9.5
2026-09-30 11:27:45 +02:00
theupriser 971e792054 chore: Version 2.9.5
Sync git.upriser.nl mirror / sync (push) Skipped
Single-file build / bundle (push) Successful in 12s
2026-09-30 11:26:15 +02:00
theupriser 6557320c81 Merge pull request #67 from theupriser/release/2.9.5
Sync git.upriser.nl mirror / sync (push) Skipped
Single-file build / bundle (push) Successful in 13s
Release/2.9.5
2026-09-30 11:21:53 +02:00
theupriser 692e3fcbad Merge pull request #65 from theupriser/feature/git-upriser-sync
Sync git.upriser.nl mirror / sync (push) Skipped
Single-file build / bundle (push) Successful in 11s
feat(ci): sync the git.upriser.nl mirror at once
2026-09-30 11:20:48 +02:00
theupriser 70ecb563fd ci: trigger a mirror sync
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 10:55:58 +02:00
theupriser a7276cfd9a ci: sync the git.upriser.nl mirror on every branch and tag
Sync git.upriser.nl mirror / sync (push) Skipped
2026-09-30 08:54:29 +02:00
theupriser b3549d4eea ci: sync the mirror on release branch pushes too (Gitea's vmtest.yml runs on them) 2026-09-30 08:50:13 +02:00
theupriser a232a55bef feat(ci): sync the git.upriser.nl mirror on every push to main and every tag
New git-upriser-sync.yml calls the mirror-sync API (secrets GIT_UPRISER_URL, GIT_UPRISER_TOKEN; retried; skipped on Gitea). bundle.yml dispatches the renamed iso-1-github-tag.yml in the right repo name; docs follow.
2026-09-30 08:12:06 +02:00
theupriser 397b3cb607 Merge pull request #64 from theupriser/release/2.9.4
Single-file build / bundle (push) Successful in 13s
Release/2.9.4
2026-09-30 07:53:56 +02:00
theupriser bda1d3ebbe fix: Merge docs/iso-repo-rename into release/2.9.4
Single-file build / bundle (push) Successful in 12s
2026-09-30 07:47:55 +02:00
theupriser 3676c64c90 docs: link the renamed steamify-cachyos-live-iso repo
- README and AGENTS.md point at theupriser/steamify-cachyos-live-iso
2026-09-30 07:47:55 +02:00
theupriser d44f29956a Merge pull request #63 from theupriser/release/2.9.4
Single-file build / bundle (push) Successful in 13s
Release 2.9.4
2026-09-29 23:08:59 +02:00
theupriser 2858d13429 docs: changelog hash for 2.9.4 2026-09-29 23:08:20 +02:00
theupriser 48cf3c7c7c Merge pull request #62 from theupriser/feature/docs-ci-and-cec-cache
docs: the CEC driver cache and the CI/ISO release flow
2026-09-29 23:08:12 +02:00
theupriser 619ef1f714 docs: AGENTS.md and TECHNICAL.md: the CEC driver cache, one workflow for GitHub and a Gitea mirror, a release starts the Steamify ISO's release 2026-09-29 23:07:28 +02:00
theupriser b6240539e6 chore: Version 2.9.4 2026-09-29 23:07:08 +02:00
theupriser 7d6df21627 Merge pull request #61 from theupriser/release/2.9.3
Single-file build / bundle (push) Successful in 13s
Release 2.9.3
2026-09-29 22:04:49 +02:00
theupriser 3cad70dcc4 docs: changelog hash for 2.9.3
Single-file build / bundle (push) Successful in 24s
2026-09-29 22:03:09 +02:00
theupriser b8742a5642 Merge pull request #60 from theupriser/feature/iso-release-on-steamify-release
feat(ci): a new Steamify release starts the Steamify ISO's release
2026-09-29 21:57:25 +02:00
theupriser 7f5d29c997 feat(ci): a new Steamify release starts the Steamify ISO's release (iso-release.yml in the ISO repo, feat/steamify; secret ISO_DISPATCH_TOKEN) 2026-09-29 21:56:42 +02:00
theupriser cfe3ce7bd0 chore: Version 2.9.3 2026-09-29 21:55:45 +02:00
theupriser 3e4133c14a Merge pull request #58 from theupriser/release/2.9.2
Single-file build / bundle (push) Successful in 14s
Release/2.9.2
2026-09-29 20:31:12 +02:00
theupriser c4b8db85dc Merge pull request #57 from theupriser/feature/gitea-release
Single-file build / bundle (push) Successful in 13s
feat(ci): a Gitea mirror publishes its own releases
2026-09-29 20:30:00 +02:00
theupriser 350011877b feat(ci): a Gitea mirror publishes its own release per version (never overwrites one, notes from the changelog) 2026-09-29 20:29:19 +02:00
theupriser b1d79d48a6 Merge pull request #55 from theupriser/release/2.9.2
Single-file build / bundle (push) Successful in 20s
chore: Version 2.9.2
2026-09-29 20:25:44 +02:00
theupriser 5b42036786 Merge pull request #56 from theupriser/bugfix/ci-shellcheck-gitea
fix(ci): the build also runs on a Gitea mirror
2026-09-29 20:25:10 +02:00
theupriser 4a64232fad fix(ci): Gitea gets christopherhx/gitea-upload-artifact@v4 (GitHub fails a workflow that mentions upload-artifact@v3) 2026-09-29 20:24:23 +02:00
theupriser 4ec92f4d89 fix(ci): the build also runs on a Gitea mirror: shellcheck through ludeeus/action-shellcheck (brings its own binary), the artifact with upload-artifact@v3 there, releases only on GitHub 2026-09-29 20:23:20 +02:00
theupriser fe4f3f43be chore: Version 2.9.2 2026-09-29 20:23:04 +02:00
theupriser 4bacc3b2e6 Merge pull request #54 from theupriser/release/2.9.1
Release/2.9.1
2026-09-29 19:51:54 +02:00
theupriser cc0ca1c35e Merge pull request #53 from theupriser/bugfix/cec-driver-cache
fix: HDMI-CEC: Valve's CEC driver is downloaded once and cached
2026-09-29 19:26:24 +02:00
theupriser 580cc71b57 docs: changelog hashes 2026-09-29 19:26:16 +02:00
theupriser c87e6c763f docs: changelog for the CEC driver cache 2026-09-29 19:19:05 +02:00
theupriser fc05725a91 fix: HDMI-CEC: an older cached CEC driver is removed when a new pin is downloaded 2026-09-29 19:18:55 +02:00
theupriser 90bc5e4408 fix: HDMI-CEC: Valve's CEC driver is downloaded once and kept in /var/cache/steamify (GitHub rate-limits repeated downloads with HTTP 429, which left CEC off) 2026-09-29 19:16:06 +02:00
theupriser d99ff652dc chore: Version 2.9.1 2026-09-29 19:15:42 +02:00
theupriser 2aa3068b60 Merge pull request #51 from theupriser/release/2.9.0
Release 2.9.0: docs (KDE Plasma only)
2026-09-28 20:42:12 +02:00
theupriser 1a6ddcfc0d Merge pull request #52 from theupriser/feature/changelog-hash-fix
docs: changelog hash fix
2026-09-28 19:48:45 +02:00
theupriser 11668db91c docs: changelog hash for 66ea63c (was the merge commit's) 2026-09-28 19:48:36 +02:00
theupriser 3316c1de28 Merge pull request #50 from theupriser/feature/changelog-hashes
docs: changelog hashes
2026-09-28 19:47:55 +02:00
theupriser d9365dac81 docs: changelog hashes 2026-09-28 19:47:47 +02:00
theupriser 853a3926ee Merge pull request #49 from theupriser/feature/docs-not-supported
docs: other desktops are not supported
2026-09-28 19:47:35 +02:00
theupriser 66ea63cf7e docs: KDE Plasma only: other desktops and display managers "are not supported" (README, was "don't work yet") 2026-09-28 19:47:28 +02:00
theupriser f560f332a9 Merge pull request #48 from theupriser/feature/docs-kde-only
docs: KDE Plasma only
2026-09-28 19:46:43 +02:00
theupriser 9b86cd1a6c docs: KDE Plasma only: other desktops don't work yet (README) 2026-09-28 19:46:36 +02:00
theupriser a72a4def71 Merge pull request #47 from theupriser/release/2.9.0
Release 2.9.0: docs
2026-09-28 19:38:41 +02:00
theupriser ea051c77f6 docs: changelog hash for d73e71d 2026-09-28 19:38:10 +02:00
theupriser 4e829c6111 Merge pull request #46 from theupriser/feature/docs-2.9.0
docs: 2.9.0 in README, TECHNICAL and TROUBLESHOOTING
2026-09-28 19:37:15 +02:00
theupriser d73e71d4eb docs: README, TECHNICAL and TROUBLESHOOTING for 2.9.0: Steam's System settings, serial number and Wi-Fi backend, --defaults --list, the Steamify ISO 2026-09-28 19:37:08 +02:00
theupriser 8f2d2be2aa Merge pull request #45 from theupriser/release/2.9.0
Release 2.9.0
2026-09-28 19:35:41 +02:00
theupriser 53b72c8e33 Merge pull request #44 from theupriser/feature/steamos-version
feat: Steam's OS Version steamos-3.9, OS Codename steam-machine
2026-09-28 19:31:35 +02:00
theupriser 3b4f3b2313 docs: changelog hash for a72d848 2026-09-28 19:31:28 +02:00
theupriser a72d848e78 feat: Steam's OS Version is the SteamOS release Steamify follows (steamos-3.9), its OS Codename steam-machine; About this System keeps "CachyOS Linux" (UseOSReleaseVersion) 2026-09-28 19:28:10 +02:00
theupriser 3a58c72d16 Merge pull request #43 from theupriser/bugfix/variant-version
fix: Steam's OS Variant shows Steamify's version
2026-09-28 19:22:31 +02:00
theupriser 27a40702ce fix: Steam's OS Variant shows Steamify's version (VARIANT_ID=steamify-2.9.0; Steam shows VARIANT_ID, not VARIANT) 2026-09-28 19:22:25 +02:00
theupriser 8ab066f034 Merge pull request #42 from theupriser/bugfix/no-version-id
fix: No VERSION_ID in os-release (About this System showed CachyOS Linux 2.9.0)
2026-09-28 19:17:10 +02:00
theupriser 44bfe071d6 fix: No VERSION_ID in os-release: About this System said "CachyOS Linux 2.9.0"; the version is in VARIANT ("Steamify 2.9.0") 2026-09-28 19:17:01 +02:00
theupriser e281d5e879 Merge pull request #41 from theupriser/feature/os-codename
feat: Steamify as OS variant, SteamOS release as codename; the OS name stays CachyOS
2026-09-28 19:14:50 +02:00
theupriser 435f69c151 feat: Steam's System settings: the OS name stays CachyOS (no renaming, also not in About this System), Steamify <version> as the variant, the SteamOS release it follows as the codename (steamos-3.9) 2026-09-28 19:14:43 +02:00
theupriser 9319cbca1e Merge pull request #40 from theupriser/bugfix/progress-checkmarks
fix(ui): The progress screen's checkmarks stand upright
2026-09-28 19:05:09 +02:00
theupriser cf6bee1c86 Merge release/2.9.0 into bugfix/progress-checkmarks 2026-09-28 19:05:02 +02:00
theupriser 16487ba00c Merge pull request #39 from theupriser/feature/steam-system-info
feat: Steam's System settings and About this System like on SteamOS
2026-09-28 19:04:29 +02:00
theupriser 07a94cd274 fix(ui): The progress screen's ✓ and ! stand upright: only a dot on the circle spins while a step runs (they kept the spinner's last angle) 2026-09-28 19:03:56 +02:00
theupriser ce71b34b0d feat: Steam's System settings and About this System like on SteamOS: OS name, Steamify as OS variant and version, serial number and Wi-Fi backend on a Steam Machine 2026-09-28 19:03:54 +02:00
theupriser d30d505f7d chore: Version 2.9.0 2026-09-28 19:03:28 +02:00
theupriser 9755cc3ac3 Merge pull request #38 from theupriser/bugfix/changelog-2.8.0-list
docs: CHANGELOG: 2.8.0's --defaults --list entry was missing
2026-09-28 13:02:11 +02:00
theupriser 6f26e74229 docs: CHANGELOG: 2.8.0's --defaults --list entry was missing 2026-09-28 13:01:00 +02:00
theupriser 2a01583b8f Merge pull request #37 from theupriser/release/2.8.0
Release 2.8.0
2026-09-28 12:46:15 +02:00
theupriser c40aa1f7d9 Merge pull request #36 from theupriser/feature/defaults-list
feat: steamify.sh --defaults --list
2026-09-28 12:45:27 +02:00
theupriser d204efb59d feat: steamify.sh --defaults --list: what --defaults can set up here, as JSON, for an installer page 2026-09-28 11:40:10 +02:00
theupriser 12f6cc0b3a chore: Version 2.8.0 2026-09-28 11:35:47 +02:00
theupriser b01e2c5221 Merge pull request #35 from theupriser/release/2.7.0
Release 2.7.0
2026-09-28 10:57:45 +02:00
theupriser affb5f79b0 Merge pull request #34 from theupriser/bugfix/theme-off-single-launcher
fix: theme off no longer brings back single user's launcher restriction
2026-09-28 10:53:19 +02:00
theupriser 58e43d4c4a fix: turning the theme off no longer brings back single user's launcher restriction when single user was turned off in between 2026-09-28 10:52:28 +02:00
theupriser 23f265b02f Merge pull request #33 from theupriser/feature/remove-v2-ui-preview
chore: remove the v2 app preview
2026-09-28 10:47:10 +02:00
theupriser ced4a45d2f chore: the v2 app preview is gone (it is the released app): no preview pre-release step in CI, no mention in steamify-app.sh 2026-09-28 10:46:23 +02:00
theupriser 6ad686a8b2 fix: Merge branch 'feature/gh-required' into release/2.7.0 2026-09-28 10:19:06 +02:00
theupriser b1adacbcb3 docs: gh is required for the release-branch pull requests; ask the user to install it 2026-09-28 10:18:59 +02:00
theupriser 8dd2db3a97 fix: Merge branch 'feature/release-branches' into release/2.7.0 2026-09-28 10:18:06 +02:00
theupriser 501c0ba425 docs: gh or git for merging into the release branch 2026-09-28 10:17:59 +02:00
theupriser bee18b76a8 docs: the agent merges feature/bugfix branches into the release branch; the release into main stays the user's 2026-09-28 10:17:41 +02:00
theupriser eadbdda271 docs: release branches (release/X.Y.Z, feature/ and bugfix/ branches into it, the release into main); CI builds release/** pushes 2026-09-28 10:14:20 +02:00
theupriser c4ec755a4a feat: --defaults --options <ids> (exactly these on) replaces --skip; steamify.sh --boot gamescope|desktop on its own switches where an installed conversion starts 2026-09-28 10:06:45 +02:00
theupriser 14aa7c0b54 fix: the app's progress screen says "Boot into desktop", not "Boot into Boot into" 2026-09-28 10:06:32 +02:00
theupriser 5bf9da411d fix: HDMI-CEC enables cec-audio-control.socket (the TV remote's volume keys); existing installs get it as an update 2026-09-28 10:06:23 +02:00
theupriser 4f6e6fd0aa feat: --defaults takes --skip <ids> and --boot gamescope|desktop, for the Steam Machine ISO's installer pages 2026-09-27 21:59:08 +02:00
theupriser a79eeedbc4 Merge pull request #32 from theupriser/feat/defaults-mode
feat: --defaults applies the default setup without the menu, also fro…
2026-09-27 21:25:03 +02:00
theupriser b948c2020b feat: --defaults applies the default setup without the menu, also from an installer (no session): the base for a Steam Machine ISO
systemctl --user goes through user_systemctl, which only changes unit files
without a session; the theme lets Plasma build Vapor's layout at the first
login when the layout is still /etc/skel's; a one-time first-login autostart
sets up single user's launcher and opens the app.
2026-09-27 19:21:28 +02:00
theupriser 0c96bed448 Merge pull request #31 from theupriser/docs/screenshot-2.5.2
docs: README screenshot at 2.5.2: everything on, nothing to update
2026-09-27 18:53:56 +02:00
theupriser e3f7cc9773 docs: README screenshot at 2.5.2: everything on, nothing to update 2026-09-27 18:53:23 +02:00
theupriser 353de489d7 Merge pull request #30 from theupriser/feat/apply-disabled
2.5.1: Add as non-Steam game, controller and apply button
2026-09-27 18:49:20 +02:00
theupriser d8b2e812c5 fix: Add as non-Steam game asks Steam to close again every 15 s (a Steam that was still starting ignored it) 2026-09-27 18:42:12 +02:00
theupriser 3a0692fdf2 docs: README screenshot at 2.5.1 with Add as non-Steam game 2026-09-27 18:17:41 +02:00
theupriser e86ddcc4af feat: Add as non-Steam game: Steamify in the Steam library, under the shortcut; controller works when started from Steam 2026-09-27 18:16:14 +02:00
theupriser 389bc3346e fix(ui): B never quits (Steam sends Esc for it), Ctrl+Q does; started from Steam, the hints show the Steam Controller's buttons 2026-09-27 17:56:57 +02:00
theupriser c6c1150836 fix(ui): Enter selects (a Steam Controller's A in Steam's desktop layout); Ctrl+Enter reviews and applies 2026-09-27 17:54:55 +02:00
theupriser 98e2effcff fix(ui): Started from Steam, the app runs without Steam's overlay (Qt aborted) and stays in the foreground, so Steam Input gives it the controller 2026-09-27 17:44:38 +02:00
theupriser 431515b60f feat(ui): Review & apply greyed out while nothing would change; the Done screen shows why a run stopped 2026-09-27 17:11:46 +02:00
theupriser 34c9cfdb32 Merge pull request #29 from theupriser/feat/update-notifier
Feat/update notifier
2026-09-27 17:02:15 +02:00
theupriser 8ddb7ebf3d docs: README: what the VRAM booster says with NVIDIA's closed driver; shorter unsupported text in the app 2026-09-27 17:00:07 +02:00
theupriser 32c33e4abf fix: An option left unticked stays off: it came back ticked as "new" on the next run (since 2.3.0) 2026-09-27 16:54:27 +02:00
theupriser 2dd0f3ac47 feat: VRAM booster with NVIDIA says what to do: update, switch to the open driver (chwd command), or not supported by the card 2026-09-27 16:31:39 +02:00
theupriser 57995bcd1f docs: VRAM booster works with NVIDIA's open kernel modules (driver 615+); only the closed driver is greyed out 2026-09-27 16:25:05 +02:00
theupriser d57e8d8448 docs: AGENTS.md and changelog for the vidmem detection 2026-09-27 16:20:57 +02:00
theupriser 5a34633986 feat: VRAM booster also for drivers that name their region vidmem (or numbered), NVIDIA included once its driver registers it 2026-09-27 16:20:36 +02:00
theupriser 479b70b337 feat: Everything in the home folder under steamify (state, scripts, icon), moved once from the old name 2026-09-27 16:11:32 +02:00
theupriser 2e0881b4fa fix: fill takes values literally; the notifier path is quoted in its unit 2026-09-27 16:05:20 +02:00
theupriser cce31e19c2 refactor: DKMS confs in patches/; an existing leds-valve DKMS override is kept and restored 2026-09-27 16:00:39 +02:00
theupriser bd4fa3a6f4 refactor: The session sync and kernel headers scripts in patches/ 2026-09-27 15:59:17 +02:00
theupriser 8ffa0f2449 refactor: systemd units in services/, read with service_file like patches/ 2026-09-27 15:58:41 +02:00
theupriser 8a2601d73d docs: README screenshot at 2.5.0 with Update notifications; AGENTS.md: retake it when rows change 2026-09-27 15:57:11 +02:00
theupriser 572417bd7b fix: Open Steamify from the notification starts the app (own scope), the same way it was last used (app or terminal) 2026-09-27 15:54:26 +02:00
theupriser cf73229756 feat: Update notifications: a notification and tray icon when there's a new Steamify 2026-09-27 14:06:48 +02:00
theupriser febdab5b53 Merge pull request #28 from theupriser/docs/readme-screenshot
2.4.1: Screenshot of the app's menu in the README
2026-09-27 13:53:41 +02:00
theupriser 90cdfd6666 chore: Version 2.4.1 2026-09-27 13:53:11 +02:00
theupriser 6f598d18fd docs: README screenshot shows every option 2026-09-27 13:52:59 +02:00
theupriser 9bbe5fa602 docs: Screenshot of the app's menu in the README 2026-09-27 13:52:59 +02:00
theupriser ceeb949e6b Merge pull request #27 from theupriser/refactor/qml-screens
2.4.0: One QML file per screen, HDMI setup removed
2026-09-27 13:49:43 +02:00
theupriser 03a8b8c4ba chore: Version 2.4.0 2026-09-27 13:48:13 +02:00
theupriser e699a3f8eb refactor(ui): One QML file per screen; the HDMI boost's setup screens and backend commands removed 2026-09-27 13:48:02 +02:00
theupriser 7bef834b16 Merge pull request #26 from theupriser/feat/vram-booster
2.3.0: VRAM booster, New badge
2026-09-27 13:05:19 +02:00
theupriser 86b9744227 feat: VRAM booster greyed out with an NVIDIA card, with why 2026-09-27 13:02:13 +02:00
theupriser b0947d7a2f feat: VRAM booster only for GPUs with at least 2 GB of their own VRAM 2026-09-27 12:55:40 +02:00
theupriser 3f97c85c3c feat: VRAM booster for every supported GPU, not only the Steam Machine 2026-09-27 12:51:47 +02:00
theupriser fd162c1e2e feat: New badge for default sub-options added in this version 2026-09-27 12:49:49 +02:00
theupriser 54ebddfe98 feat: VRAM booster for the Steam Machine 2026-09-27 12:48:01 +02:00
theupriser fe93dd3e67 Merge pull request #25 from theupriser/feat/fremont-poweroff
Feat/fremont poweroff
2026-09-27 12:41:38 +02:00
theupriser ef585c0db1 docs: Changelog entries per commit for 2.2.0 2026-09-27 12:36:25 +02:00
theupriser 32795bd5a5 feat(ui): Update badge on the progress screen 2026-09-27 12:35:40 +02:00
theupriser 3c8ca65902 fix(ui): Never write the sudo password to a file; Update label on the progress screen
The askpass helper reads the password from a named pipe that the app
feeds from memory each time sudo asks, instead of from a file.
2026-09-27 12:33:47 +02:00
theupriser 11a91ef85c feat: Update badge on components that get a newer version 2026-09-27 12:31:29 +02:00
theupriser 163f7908c4 feat: Feature versions are Steamify versions; Steam Machine support is 2.2.0 2026-09-27 12:27:55 +02:00
theupriser e121b06fd8 fix: Remove whatever is left of HDMI refresh boost, pin or no pin 2026-09-27 12:27:08 +02:00
theupriser 19ca966913 docs: Drop the kernel pin and HDMI refresh boost descriptions 2026-09-27 12:25:08 +02:00
theupriser 56fb9080a7 docs: Power-off fix texts cover every affected kernel, not just 7.2 2026-09-27 12:23:54 +02:00
theupriser 6df19eba3e feat: Power-off fix as its own sub-option, drop the kernel pin, feature versions
The power-off fix becomes an opt-out sub-option of Steam Machine
support. The kernel pin is no longer offered: an existing pin (and HDMI
refresh boost, which needs it) is unticked so a normal run removes it.
Components record a feature version on each successful run; installs
with an older version are ticked and updated, and new default
sub-options are ticked where their parent is on.
2026-09-27 12:20:56 +02:00
theupriser 7694c97d68 feat: Power-off fix for the Steam Machine; the kernel pin is optional
Kernels >= 7.2 keep the firmware's S4/S5 wake bit on GPIO pin 18, so the
Steam Machine starts again right after powering off. Steam Machine
support now builds a DKMS module (patches/steamify-fremont-poweroff.c)
that clears it before power-off, for every kernel. The kernel pin is no
longer ticked by default. Module sources and patches move to patches/,
embedded by the single-file build.
2026-09-27 12:13:23 +02:00
theupriser a5e644d71a Merge pull request #24 from theupriser/feat/hdmi-per-display
Feat/hdmi per display
2026-09-26 16:49:56 +02:00
theupriser 929ecfb349 feat(ui): Manage button for HDMI refresh boost
The item's switch becomes a Set up / Manage button. Manage lists the
saved displays, removes one with its Remove button, and sets up the
connected display when it has no saved rates.
2026-09-26 16:48:21 +02:00
theupriser 4cc87aaea6 feat: HDMI refresh boost per display, with a Saved displays screen
Rates are saved per display (EDID ID) and loaded by the hotplug script
for whichever display is connected. The item is on only while the
connected display is boosted; unticking removes just that one. The app
lists the saved displays and removes any of them (hdmi-forget).
2026-09-26 16:13:45 +02:00
theupriser 36d0a9d780 feat: HDMI refresh boost only for the display it was set up for
The EDID override moves off the kernel command line to a hotplug script
(boot unit + udev rule) that loads it only while the display it was
tested on is connected, identified over DDC; other displays get their
own EDID.
2026-09-26 16:04:02 +02:00
theupriser cb6361b49b Merge pull request #23 from theupriser/fix/fremont-cec
fix: HDMI-CEC on the Steam Machine
2026-09-26 11:30:29 +02:00
theupriser c510f9e2e5 fix: HDMI-CEC on the Steam Machine 2026-09-26 11:29:34 +02:00
theupriser 05a7d50c3f Merge pull request #22 from theupriser/docs/disclaimer
docs: Non-affiliation disclaimer in the README
2026-09-26 10:54:06 +02:00
theupriser badfd846d8 docs: Non-affiliation disclaimer in the README 2026-09-26 10:53:42 +02:00
theupriser ff364ffd69 Merge pull request #21 from theupriser/feat/app-shortcut
feat: The Steamify shortcut opens the app
2026-09-25 21:33:14 +02:00
theupriser 2b3f1de75a feat: The Steamify shortcut opens the app
Steamify CachyOS on the desktop and in the launcher starts the newest app
(in Konsole the first time, while PySide6 still has to be installed); the
terminal menu is Steamify Terminal in the launcher. A shortcut from before
2.0.1 is ticked in the menu, so a normal run replaces it. steamify-app.sh
leaves the shortcut's launcher entry (same id) alone.
2026-09-25 21:32:24 +02:00
theupriser 6d7d5861d6 Merge pull request #20 from theupriser/feat/v2-ui
Feat/v2 UI
2026-09-25 21:24:11 +02:00
theupriser 444ce29e57 Merge remote-tracking branch 'origin/main' into feat/v2-ui
# Conflicts:
#	AGENTS.md
#	CHANGELOG.md
#	lib/hdmi-refresh.sh
#	steamify.sh
2026-09-25 21:21:31 +02:00
theupriser 23ef5ef7ab feat(ui): HDMI refresh boost in the app
Set up… opens a screen with the display, its resolution and the rates on
offer. The display's own faster mode is switched to right away; the other
rates can each be tried live (wait for the picture, keep within 15 s or it
switches back) and ticked; Install installs the ticked ones through
apply --hdmi. All rates are loaded when the screen opens, so a try is only a
mode switch. Also fixes switching back to a rate like 59.97 Hz.
2026-09-25 21:21:13 +02:00
theupriser 178ee3405e fix(ui): The details panel scrolls instead of overflowing
Long texts (e.g. HDMI refresh boost) ran out of the card. The details are
in a Flickable now, which starts at the top for each item.
2026-09-25 20:42:08 +02:00
theupriser 601c05329d chore: Version 2.0.0 for the v2 app 2026-09-25 20:39:57 +02:00
theupriser 8ae9673eb2 Merge pull request #19 from theupriser/feat/hdmi-refresh-boost
Feat/hdmi refresh boost
2026-09-25 20:39:13 +02:00
theupriser dbafc43f2e feat(ui): HDMI refresh boost in the app, off only
The app gets its texts and the same rules as the menu: unticking Steam
Machine support or the kernel pin unticks it. It can't be turned on from
the app, as each step needs someone at the keyboard to confirm the picture.
2026-09-25 20:39:00 +02:00
theupriser ae8ed85fee feat: HDMI refresh boost and Update BIOS under Steam Machine support
Both are sub-options of Steam Machine support now, shown only while it is
ticked. Unticking it unticks them; unticking the kernel pin unticks the
HDMI refresh boost, which only works with the pinned kernel.
2026-09-25 20:37:56 +02:00
theupriser 00bcaea712 feat: HDMI refresh boost and Update BIOS under Steam Machine support
Both are sub-options of Steam Machine support now, shown only while it is
ticked. Unticking it unticks them; unticking the kernel pin unticks the
HDMI refresh boost, which only works with the pinned kernel.
2026-09-25 20:37:47 +02:00
theupriser a898d23b13 feat: HDMI refresh boost (Steam Machine, pinned kernel)
The pinned 7.1.6 kernel doesn't read the extra EDID block (HDMI Forum
EEODB) where monitors list their fast modes, and does no HDMI 2.1, so HDMI
displays often stay at 60 Hz. The new menu item reads the display's EDID
over DDC, calculates the highest rate that fits HDMI 2.0 at the desktop
resolution (rounded down to ten, plus the hundred below as a safe option),
tests each step live with a confirmation, and makes the confirmed ones
permanent through drm.edid_firmware (Limine, sdboot-manage or GRUB).
2026-09-25 20:33:46 +02:00
theupriser 3e53257fe2 feat: HDMI refresh boost (Steam Machine, pinned kernel)
The pinned 7.1.6 kernel doesn't read the extra EDID block (HDMI Forum
EEODB) where monitors list their fast modes, and does no HDMI 2.1, so HDMI
displays often stay at 60 Hz. The new menu item reads the display's EDID
over DDC, calculates the highest rate that fits HDMI 2.0 at the desktop
resolution (rounded down to ten, plus the hundred below as a safe option),
tests each step live with a confirmation, and makes the confirmed ones
permanent through drm.edid_firmware (Limine, sdboot-manage or GRUB).
2026-09-25 20:32:46 +02:00
theupriser a1eab9ac79 feat: Preview builds of feat/v2-ui; the app stretches to the window
Pushes to feat/v2-ui publish the pre-release v2-ui-preview (never latest);
its steamify-app.sh downloads that build (STEAMIFY_RELEASE). The app's
1280x720 layout now stretches to the window's shape, so the bars reach the
edges; re-apply resets when back on the menu.
2026-09-25 16:04:59 +02:00
theupriser c9c16b9eec fix(ui): Review and progress lists scroll; progress follows the running step 2026-09-25 16:02:19 +02:00
theupriser b17addd60f fix: Quote the backend's done event for shellcheck 2026-09-25 16:00:12 +02:00
theupriser ac613f05b3 feat: GitHub builds the app; run it with curl
steamify-app.tar.gz (ui/, steamify.sh, lib/, icon) and its launcher
steamify-app.sh in every release: it installs to ~/.local/share/steamify/app,
installs PySide6 if needed, adds a launcher entry and starts the app.
STEAMIFY_BRANCH=<branch> runs a branch before it's released.
2026-09-25 15:59:47 +02:00
theupriser 3a0eefab11 fix(ui): Applying got stuck: the ticked items came in as a JS value
Convert QML's array; if starting fails anyway, show the done screen with
the reason instead of staying on the progress screen.
2026-09-25 15:56:41 +02:00
theupriser 96558a9ca8 feat(ui): BIOS check runs invisibly, then the warnings, then the progress bar 2026-09-25 15:55:15 +02:00
theupriser 7c48b3d806 feat: BIOS update in the app, with both warnings
bios_enable split into bios_prepare (download, checksum, fwupd check) and
bios_flash; the terminal flow is unchanged. The app: check, warning 1,
warning 2 (type UPDATE, or hold A/OK for 5 seconds), flash, restart.
2026-09-25 15:53:30 +02:00
theupriser 396459b511 fix(ui): Controls aligned on the right, sub-items fold instead of the list jumping 2026-09-25 15:48:38 +02:00
theupriser 43a9541104 feat: Steamify app (v2 UI), first working version
ui/steamify-ui (PySide6 + QML): menu, review, password, applying and done
screens from the design, scaled to the window. Controller via evdev,
keyboard, TV remote (cecd). steamify.sh --backend status|apply (lib/backend.sh)
drives the same components; sudo asks through an askpass helper.
2026-09-25 15:46:02 +02:00
theupriser e09fe1609b Merge pull request #18 from theupriser/fix/no-lock-before-restart
fix: No lock screen in single user mode before a restart
2026-09-25 14:10:52 +02:00
theupriser 9dcac5cf08 fix: No lock screen in single user mode before a restart
The screen locker only read kscreenlockerrc at the next login, so the PC
still locked after idling until then. Reload it over D-Bus.
2026-09-25 14:01:00 +02:00
theupriser f91dd1f0b1 Merge pull request #16 from theupriser/fix/cec-steam-ui
Fix/cec steam UI
2026-09-25 13:18:52 +02:00
theupriser 0411429654 chore: Version 1.1.3 2026-09-25 13:17:02 +02:00
theupriser 1aa5e955fe fix: Repair HDMI-CEC set up by 1.1.0/1.1.1
It only counts as on with the steam-launcher drop-in; installed without it,
the menu ticks it, so a normal run adds it.
2026-09-25 13:16:48 +02:00
theupriser ce75e895c4 fix: Show the HDMI-CEC settings in Steam
CachyOS's gaming mode script sets STEAM_ENABLE_CEC=0. A drop-in for
steam-launcher.service overrides it with a later EnvironmentFile=.
2026-09-25 13:16:48 +02:00
79 changed files with 5384 additions and 311 deletions

No files matched your search

+22
View File
@@ -31,6 +31,28 @@ version="v$(grep -oP '^VERSION=\K.*' "$entry"), $(git describe --always --dirty
sed -e '1{/^#!/d}' -e '/^# Sourced by steamify.sh/d' "lib/$lib.sh" sed -e '1{/^#!/d}' -e '/^# Sourced by steamify.sh/d' "lib/$lib.sh"
echo echo
done done
# patches/ and services/ embedded: replace lib/common.sh's patch_file
# and service_raw, which read them from the checkout.
embed() {
local fn="$1" dir="$2" f
echo "# ----- $dir/ -----"
echo "$fn() {"
echo ' case "$1" in'
for f in "$dir"/*; do
[[ "$f" == "$dir/README.md" ]] && continue
grep -q '^STEAMIFY_PATCH_EOF$' "$f" && { echo "bundle: $f contains the heredoc delimiter" >&2; exit 1; }
echo " $(basename "$f")) cat << 'STEAMIFY_PATCH_EOF'"
cat "$f"
echo "STEAMIFY_PATCH_EOF"
echo " ;;"
done
echo ' *) return 1 ;;'
echo ' esac'
echo "}"
echo
}
embed patch_file patches
embed service_raw services
echo "# ----- steamify.sh -----" echo "# ----- steamify.sh -----"
echo "main() {" echo "main() {"
# Everything after the source loop is the entry point's body. # Everything after the source loop is the entry point's body.
+134 -6
View File
@@ -5,12 +5,16 @@
# release are moved to it, so both of these always get the newest version: # release are moved to it, so both of these always get the newest version:
# curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh | bash # curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh | bash
# curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/download/latest/steamify.sh | bash # curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/download/latest/steamify.sh | bash
# Each release also has the app: steamify-app.tar.gz (ui/, steamify.sh,
# lib/, patches/, services/, the icon) and its launcher, run with:
# curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify-app.sh | bash
# Pushes to release/** build and check it like pull requests; only main publishes.
# Pull requests only build and check it, and warn when VERSION is already released. # Pull requests only build and check it, and warn when VERSION is already released.
name: Single-file build name: Single-file build
on: on:
push: push:
branches: [main] branches: [main, 'release/**']
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
@@ -28,20 +32,57 @@ jobs:
- name: Build - name: Build
run: | run: |
.github/tools/bundle.sh dist/steamify.sh .github/tools/bundle.sh dist/steamify.sh
# The app: sources, not the single-file build (its backend mode runs
# steamify.sh from its own folder).
tar -czf dist/steamify-app.tar.gz steamify.sh lib patches services ui assets/steam-gaming-settings.svg LICENSE.md
cp ui/steamify-app.sh dist/steamify-app.sh
- name: Check - name: Check
run: | run: |
bash -n dist/steamify.sh bash -n dist/steamify.sh
# SC2034/SC2154: variables used across the inlined modules. python3 -m py_compile ui/steamify-ui
shellcheck -S warning -e SC2034,SC2154 dist/steamify.sh
# shellcheck through an action that brings its own binary: GitHub's runner has it installed,
# Gitea's act_runner image doesn't. One file per run, as before (the exceptions are the bundle's).
- name: shellcheck the bundle
uses: ludeeus/action-shellcheck@2.0.0
env:
# SC2034/SC2154: variables used across the inlined modules.
SHELLCHECK_OPTS: -e SC2034 -e SC2154
with:
severity: warning
scandir: dist
ignore_names: steamify-app.sh
- name: shellcheck the app launcher
uses: ludeeus/action-shellcheck@2.0.0
with:
severity: warning
scandir: dist
ignore_names: steamify.sh
# The build as an artifact on GitHub and on the mirror (Gitea); releases stay GitHub's.
# Gitea's artifact server doesn't speak upload-artifact@v4's protocol, and GitHub fails a workflow
# that merely mentions the deprecated @v3: Gitea gets christopherhx's fork of v4 for Gitea.
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: github.server_url == 'https://github.com'
with: with:
name: steamify name: steamify
path: dist/steamify.sh path: |
dist/steamify.sh
dist/steamify-app.tar.gz
dist/steamify-app.sh
- uses: christopherhx/gitea-upload-artifact@v4
if: github.server_url != 'https://github.com'
with:
name: steamify
path: |
dist/steamify.sh
dist/steamify-app.tar.gz
dist/steamify-app.sh
- name: Version - name: Version
id: version id: version
if: github.server_url == 'https://github.com'
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
@@ -56,6 +97,7 @@ jobs:
- name: Publish the v${{ steps.version.outputs.version }} release - name: Publish the v${{ steps.version.outputs.version }} release
if: >- if: >-
github.server_url == 'https://github.com' &&
github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' &&
steps.version.outputs.released == 'false' steps.version.outputs.released == 'false'
env: env:
@@ -71,9 +113,13 @@ jobs:
curl -fsSL https://github.com/${{ github.repository }}/releases/latest/download/steamify.sh | bash curl -fsSL https://github.com/${{ github.repository }}/releases/latest/download/steamify.sh | bash
Or the app (v2):
curl -fsSL https://github.com/${{ github.repository }}/releases/latest/download/steamify-app.sh | bash
## Changes in v$VERSION ## Changes in v$VERSION
$changes" $changes"
gh release create "v$VERSION" dist/steamify.sh assets/steam-gaming-settings.svg \ gh release create "v$VERSION" dist/steamify.sh dist/steamify-app.sh dist/steamify-app.tar.gz assets/steam-gaming-settings.svg \
--target "$GITHUB_SHA" --title "Steamify CachyOS v$VERSION" --notes "$notes" --latest --target "$GITHUB_SHA" --title "Steamify CachyOS v$VERSION" --notes "$notes" --latest
# "latest" follows the newest version tag, for the older install URL # "latest" follows the newest version tag, for the older install URL
@@ -86,4 +132,86 @@ jobs:
else else
gh release create latest --title "Steamify CachyOS - latest (v$VERSION)" --notes "$latest_notes" --latest=false gh release create latest --title "Steamify CachyOS - latest (v$VERSION)" --notes "$latest_notes" --latest=false
fi fi
gh release upload latest dist/steamify.sh assets/steam-gaming-settings.svg --clobber gh release upload latest dist/steamify.sh dist/steamify-app.sh dist/steamify-app.tar.gz assets/steam-gaming-settings.svg --clobber
# The mirror (Gitea) publishes its own release per version too, the same way: never overwrites one,
# notes from the changelog. gh can't talk to Gitea: its API for the check, a Gitea release action to
# publish. The token is the run's own (repository write); a secret RELEASE_TOKEN overrides it.
- name: Version (mirror)
id: mirror_version
if: github.server_url != 'https://github.com'
env:
TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }}
run: |
version="$(grep -oP '^VERSION=\K.*' steamify.sh)"
echo "version=$version" >> "$GITHUB_OUTPUT"
if curl -fsS -o /dev/null -H "Authorization: token $TOKEN" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/tags/v$version"; then
echo "released=true" >> "$GITHUB_OUTPUT"
echo "::warning::v$version is already released on this server and won't be overwritten."
else
echo "released=false" >> "$GITHUB_OUTPUT"
fi
- name: Release notes (mirror)
if: >-
github.server_url != 'https://github.com' &&
github.event_name != 'pull_request' && github.ref == 'refs/heads/main' &&
steps.mirror_version.outputs.released == 'false'
env:
VERSION: ${{ steps.mirror_version.outputs.version }}
run: |
changes="$(awk -v v="$VERSION" '
$0 ~ "^## " { if (found) exit; if (index($0, "## " v " ") == 1) { found = 1; next } }
found' CHANGELOG.md)"
cat > dist/release-notes.md <<NOTES
Single-file build of v$VERSION ($(git describe --always), $(git log -1 --format=%cs)).
Run this version with:
curl -fsSL ${{ github.server_url }}/${{ github.repository }}/releases/download/v$VERSION/steamify.sh | bash
## Changes in v$VERSION
$changes
NOTES
- name: Publish the v${{ steps.mirror_version.outputs.version }} release (mirror)
if: >-
github.server_url != 'https://github.com' &&
github.event_name != 'pull_request' && github.ref == 'refs/heads/main' &&
steps.mirror_version.outputs.released == 'false'
uses: akkuman/gitea-release-action@v1.3.7
with:
token: ${{ secrets.RELEASE_TOKEN || github.token }}
tag_name: v${{ steps.mirror_version.outputs.version }}
target_commitish: ${{ github.sha }}
name: Steamify CachyOS v${{ steps.mirror_version.outputs.version }}
body_path: dist/release-notes.md
files: |
dist/steamify.sh
dist/steamify-app.sh
dist/steamify-app.tar.gz
assets/steam-gaming-settings.svg
# A new version starts the Steamify ISO's release of the ISO repo: its iso-1-github-tag.yml (always run from
# master) names the ISO after the newest Steamify release, tags it, and the Gitea mirror builds and attaches the ISO.
# The branch here decides the kind of build (the workflow's input `kind`):
# main (publishes the release) -> release: a real ISO (tag v<steamify>-<date>)
# release/** (a version not yet released) -> dev: a pre-release (tag v<steamify>-dev.<date>)
# Needs a fine-grained token for that repo (Actions: read and write) as the secret ISO_DISPATCH_TOKEN;
# without it, this is skipped.
- name: Start the Steamify ISO's release
if: >-
github.server_url == 'https://github.com' &&
github.event_name != 'pull_request' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) &&
steps.version.outputs.released == 'false'
env:
GH_TOKEN: ${{ secrets.ISO_DISPATCH_TOKEN }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::notice::No ISO_DISPATCH_TOKEN: the Steamify ISO isn't released with this version."; exit 0
fi
# A release branch builds the ISO with its own code (and VERSION): steamify_ref. main builds the release it just published.
if [ "$GITHUB_REF" = refs/heads/main ]; then kind=release; sref=""; else kind=dev; sref="$GITHUB_REF_NAME"; fi
gh workflow run iso-1-github-tag.yml -R theupriser/steamify-cachyos-live-iso --ref master -f kind="$kind" -f steamify_ref="$sref"
echo "Started the Steamify ISO's release (theupriser/steamify-cachyos-live-iso, master, kind $kind, Steamify ${sref:-the newest release})"
+26
View File
@@ -0,0 +1,26 @@
name: Sync git.upriser.nl mirror
# Trigger the Gitea pull-mirror immediately instead of waiting for its interval,
# so Gitea builds/tests right after any branch or tag lands on GitHub (the mirror has no sync interval of its own).
on:
push:
branches: ['**']
tags: ['*']
workflow_dispatch:
jobs:
sync:
# Gitea also reads .github/workflows on the mirror; only run on GitHub
if: github.server_url == 'https://github.com'
runs-on: ubuntu-latest
steps:
- name: Trigger mirror-sync
env:
GITEA_URL: ${{ secrets.GIT_UPRISER_URL }}
GIT_UPRISER_TOKEN: ${{ secrets.GIT_UPRISER_TOKEN }}
# Gitea "owner/repo"; defaults to the same path as on GitHub
GITEA_REPO: ${{ vars.GITEA_REPO || github.repository }}
run: |
curl --fail-with-body -sS --retry 5 --retry-delay 10 --retry-all-errors -X POST \
-H "Authorization: token ${GIT_UPRISER_TOKEN}" \
"${GITEA_URL%/}/api/v1/repos/${GITEA_REPO}/mirror-sync"
+1
View File
@@ -1 +1,2 @@
dist/ dist/
__pycache__/
+198 -8
View File
@@ -15,6 +15,29 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
- `steamify.sh` - the only entry point. Pre-flight checks, - `steamify.sh` - the only entry point. Pre-flight checks,
sources `lib/*.sh`, runs the menu and applies the plan. No component sources `lib/*.sh`, runs the menu and applies the plan. No component
logic here. 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 - `lib/*.sh` - one file per responsibility, each defining functions only
(no top-level side effects besides constants). See the table in (no top-level side effects besides constants). See the table in
`TECHNICAL.md`. `TECHNICAL.md`.
@@ -25,6 +48,23 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
`<id>_available` is checked via `component_available` (e.g. `machine` `<id>_available` is checked via `component_available` (e.g. `machine`
only on Fremont). Turn-on order is `COMPONENTS` order, turn-off reverse; only on Fremont). Turn-on order is `COMPONENTS` order, turn-off reverse;
`gaming` must stay first. Dependencies live in `toggle_component`. `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"). New opt-in options (`NO_PRESELECT`) get the
same "new" badge without being ticked (`feature_new_optin`). Both count "Gaming
on NVIDIA" as a parent where the conversion isn't offered.
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 - **Reversibility.** Every per-user KDE setting a component changes goes
through `kset <component> <file> <group|group> <key> <value>` through `kset <component> <file> <group|group> <key> <value>`
(`lib/state.sh`), which records the old value once; `<id>_disable` calls (`lib/state.sh`), which records the old value once; `<id>_disable` calls
@@ -35,6 +75,13 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
the entry point's `for lib in ...; do` source loop and wraps everything 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 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. 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 CI (`.github/workflows/bundle.yml`) publishes the bundle as release
`v$VERSION` on pushes to `main`; an existing version is never overwritten, `v$VERSION` on pushes to `main`; an existing version is never overwritten,
so bump `VERSION` for every release. so bump `VERSION` for every release.
@@ -65,11 +112,56 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
+ subject; a commit can't contain its own hash, so fill it in with the + 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 next commit). The section heading must be `## <VERSION> - <date>`: CI
cuts the release notes out of the changelog by that heading. cuts the release notes out of the changelog by that heading.
- Every PR that should be released bumps `VERSION` and adds its section. - **Branches: one release branch per version.** Work for a release goes
A push to `main` publishes release `v$VERSION` (tag + bundle + that 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 changelog section) and marks it latest. An existing version is never
overwritten: without a bump nothing is released, and pull requests show overwritten: without a bump nothing is released, and pull requests show
a warning. 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 screenshots (`assets/screenshot-menu-steam-machine.png` and
`assets/screenshot-menu-nvidia.png`, side by side in the README): the app from the branch on a Steam Machine and on a PC
with an NVIDIA card, every row visible and each window only as tall as its options need, the header showing the new
`VERSION`, 1600 px wide. Size the window with a KWin script (match the `steamify-ui` class exactly: a title match also hits
Konsole and editor windows) and crop the full-screen capture to the client rectangle; mind the display scale (the Steam
Machine runs at 1.75).
- Users install through `releases/latest/download/steamify.sh` - Users install through `releases/latest/download/steamify.sh`
(GitHub's newest release). The `latest` tag and release follow the newest (GitHub's newest release). The `latest` tag and release follow the newest
version tag too (moved, asset replaced, when a new version is released), version tag too (moved, asset replaced, when a new version is released),
@@ -125,7 +217,9 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
`restart_plasmashell_if_stopped` (`lib/common.sh`). `restart_plasmashell_if_stopped` (`lib/common.sh`).
- LED driver: `leds-valve-dkms-git`'s Makefile builds against `uname -r`, - LED driver: `leds-valve-dkms-git`'s Makefile builds against `uname -r`,
not DKMS's target kernel: `/etc/dkms/leds-valve-dkms.conf` sets not DKMS's target kernel: `/etc/dkms/leds-valve-dkms.conf` sets
`MAKE[0]="make KVERSION=${kernelver}"` (written before the AUR install). `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 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 fail (CachyOS kernels are clang-built; DKMS adds `LLVM=1` only for the
target's tree). Headers for every installed kernel are installed first, target's tree). Headers for every installed kernel are installed first,
@@ -142,7 +236,7 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
`steam-set-session plasma.desktop` plus the plasmalogin sync bridge when it `steam-set-session plasma.desktop` plus the plasmalogin sync bridge when it
exists, at every boot. CachyOS's `cachyos-gamescope-autologin` still sets exists, at every boot. CachyOS's `cachyos-gamescope-autologin` still sets
gamescope during each desktop session; the unit corrects it at boot. gamescope during each desktop session; the unit corrects it at boot.
- `bios` is an *action* (`ACTIONS` in `lib/menu.sh`), not an on/off - `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 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. 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 Only selectable when Valve's version differs from the installed one
@@ -161,9 +255,14 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
now? [Y/n]" (`quit_prompt`): `n` goes back to the menu, the next `q` asks 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 again. Scripted input that runs out quits without restarting (never
restart on EOF: `ask_yn` would take its default). restart on EOF: `ask_yn` would take its default).
- Steamify shortcut (`launcher`): the icon runs `curl | bash` of - Steamify shortcut (`launcher`): "Steamify CachyOS" (desktop and launcher,
`releases/latest/download/steamify.sh` in Konsole, so it's id `steamify-ui.desktop`, the app's own id) runs `curl | bash` of
always the newest release. Its icon, `assets/steam-gaming-settings.svg` `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 (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 downloaded from there (Steam's icon if that fails). Desktop files are
written with their mode already set (`install_executable`): Plasma opens a written with their mode already set (`install_executable`): Plasma opens a
@@ -171,6 +270,97 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
The start script closes its window after a 10-second countdown on success 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 and waits for Enter after an error; it uses `pipefail`, or a failed
download would run an empty script and count as success. 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 - `Relogin=true` means a gamescope that fails to start is relaunched in a
tight loop; keep that in mind when changing session handling. tight loop; keep that in mind when changing session handling.
+282
View File
@@ -5,6 +5,288 @@ All notable changes, per version and per commit. Versions follow
`steamify.sh`. Versions before 0.7.0 were numbered afterwards, `steamify.sh`. Versions before 0.7.0 were numbered afterwards,
one per merged pull request. one per merged pull request.
## 2.11.0 - 2026-10-01
- `2e51d1d`, `3494924`, `fbdad5a`, `079e725`, `188799e` **feat: "Start Steam silently" sub-option: under the SteamOS conversion (`silent`, opt-in now: it was always on before; an existing one is moved from its systemd unit to the autostart entry by the "update") and under Gaming on NVIDIA (`nvsilent`, opt-in, not together with Big Picture); the SteamOS one is called "Start Steam silently in desktop mode" and is always shown, also on a Steam Machine; it is Steam's own autostart entry (`~/.config/autostart/steam.desktop`), so Steam's setting "Run Steam when my computer starts" follows it**
- `2f7b53f` **fix: the Steam Machine's serial number step no longer fails inside the ISO's installer (`/sys` is read-only in its chroot; the tmpfiles.d file applies at the next boot anyway), which made `machine` report an error and the install log end with `exit: 1`**
- `45a23af`, `bd8f387` **feat: `STEAMIFY_NO_DOWNLOAD` (the ISO's `steamify.nodownload` kernel parameter): the installed system's first login runs the Steamify bundle of the ISO instead of downloading the newest release (to test an unreleased Steamify)**
- `f3810a9` **fix: the SteamOS theme no longer leaves a blank Discover pin in the taskbar (Plasma's default taskbar pins Discover, which CachyOS doesn't install); the taskbar gets CachyOS's own pins (System Settings, files, browser), also on the ISO's first login**
- `7869b47` **feat: "Steam Deck/Machine icons" (`glyphs`) is a sub-option of the SteamOS conversion, like "Start Steam silently in desktop mode": it only changes gaming mode**
- `4d387b2` **chore: the Steamify unit tests (`controllers-test.sh`, `nvidia-test.sh`) and the NVIDIA hardware test live in steamify-cachyos-dev (`tests/`) now; `tests/` is removed here**
## 2.10.0 - 2026-10-01
- `d6db4f6` **feat: "Gaming on NVIDIA" (nvidia) replaces the SteamOS conversion on PCs with an NVIDIA GPU, where gamescope's gaming mode shows a corrupted picture (NVIDIA bug 5240452): Steam is installed when missing and starts at login on the Plasma desktop; the sub-option "Steam starts in Big Picture" (bigpicture) starts it in Big Picture, unticked in its normal window, on an empty Plasma session so windows of the last session don't cover it. On a supported card (RTX 20 series or newer) it also sets the NVIDIA kernel parameters and early modules. The conversion and its options are hidden there unless already on**
- `d1373e2` **feat: single user mode also works on NVIDIA PCs, without the SteamOS conversion**
- `5811f5c` **fix: "Add as non-Steam game" isn't offered on NVIDIA PCs (shown when already on, so it can be turned off)**
- `83248a8` **feat: Extended controller support (extended_controller_support, off by default): the Xbox wireless dongle (xone) and Xbox controllers over Bluetooth with rumble and battery level (xpadneo), built for every installed kernel; off removes only what Steamify installed**
- `01a77b7` **feat: a new opt-in option (unticked by default) shows the "new" badge in the menu and the app**
- `f1983a9` **docs: README screenshots of the 2.10.0 app, side by side: on a Steam Machine and on a PC with an NVIDIA card**
## 2.9.6 - 2026-09-30
- **fix(ci): a dev ISO from a release branch is built with that branch's own Steamify (input steamify_ref of the ISO workflow), not the newest published release: release/2.9.6 kept producing 2.9.5 ISOs**
- **docs: changelog hashes for 2.9.6**
- `9a2965d` **fix(ci): the ISO workflow always runs from master; main asks for a release ISO, a release branch for a dev ISO (input `kind` of iso-1-github-tag.yml; feat/steamify is no longer needed)**
- `c42d545` **fix(ci): a version published from main starts a real ISO release, a release branch a dev ISO (it always started the dev one)**
- `b3bed43` **chore: Version 2.9.6**
## 2.9.5 - 2026-09-30
- `70ecb56` **ci: trigger a mirror sync**
- `a7276cf` **ci: sync the git.upriser.nl mirror on every branch and tag**
- `b3549d4` **ci: sync the mirror on release branch pushes too**
- `a232a55` **feat(ci): sync the git.upriser.nl mirror on every push to main and every tag (git-upriser-sync.yml calls the mirror-sync API: the Gitea mirror builds at once instead of at its interval; secrets GIT_UPRISER_URL and GIT_UPRISER_TOKEN, retried, skipped on Gitea itself)**
- `3676c64` **docs: link the renamed steamify-cachyos-live-iso repo (the release dispatches the ISO workflow iso-1-github-tag.yml there)**
- `971e792` **chore: Version 2.9.5**
## 2.9.4 - 2026-09-29
- `619ef1f` **docs: AGENTS.md and TECHNICAL.md: the CEC driver cache, the one workflow for GitHub and a Gitea mirror, and that a new release starts the Steamify ISO's release**
- `b624053` **chore: Version 2.9.4**
## 2.9.3 - 2026-09-29
- `7f5d29c` **feat(ci): a new Steamify release starts the Steamify ISO's release (a test build from the ISO repo's feat/steamify; secret ISO_DISPATCH_TOKEN)**
- `cfe3ce7` **chore: Version 2.9.3**
## 2.9.2 - 2026-09-29
- **feat(ci): a Gitea mirror publishes its own release per version (akkuman/gitea-release-action; never overwrites one, notes from the changelog, links to the mirror)**
- **fix(ci): the build also runs on a Gitea mirror (act_runner): shellcheck through ludeeus/action-shellcheck, which brings its own binary; the artifact goes up there with christopherhx/gitea-upload-artifact (GitHub refuses a workflow that mentions the deprecated upload-artifact@v3); releases are only published on GitHub**
- `fe4f3f4` **chore: Version 2.9.2**
## 2.9.1 - 2026-09-29
- `c87e6c7` **docs: changelog for the CEC driver cache**
- `fc05725` **fix: HDMI-CEC: an older cached CEC driver is removed when a new pin is downloaded**
- `90bc5e4` **fix: HDMI-CEC: Valve's CEC driver is downloaded once and kept in /var/cache/steamify (GitHub rate-limits repeated downloads with HTTP 429, which left CEC off); turning CEC on again needs no network**
- `d99ff65` **chore: Version 2.9.1**
## 2.9.0 - 2026-09-28
- `d30d505` **chore: Version 2.9.0**
- `66ea63c` **docs: KDE Plasma only: other desktops and display managers "are not supported" (README, was "don't work yet")**
- `9b86cd1` **docs: KDE Plasma only: other desktops don't work yet (README)**
- `d73e71d` **docs: README, TECHNICAL and TROUBLESHOOTING for 2.9.0: Steam's System settings, serial number and Wi-Fi backend, --defaults --list, the Steamify ISO**
- `a72d848` **feat: Steam's OS Version is the SteamOS release Steamify follows (steamos-3.9), its OS Codename steam-machine; About this System keeps "CachyOS Linux" (UseOSReleaseVersion)**
- `27a4070` **fix: Steam's OS Variant shows Steamify's version (VARIANT_ID=steamify-2.9.0; Steam shows VARIANT_ID, not VARIANT)**
- `44bfe07` **fix: No VERSION_ID in os-release: About this System said "CachyOS Linux 2.9.0"; the version is in VARIANT ("Steamify 2.9.0")**
- `435f69c` **feat: Steam's System settings: the OS name stays CachyOS (no renaming, also not in About this System), Steamify <version> as the variant, the SteamOS release it follows as the codename (steamos-3.9)**
- `ce71b34` **feat: Steam's System settings and About this System like on SteamOS: OS name "CachyOS with Steamify" (lsb-release, kept through CachyOS updates by a pacman hook), Steamify as OS variant and version (os-release VARIANT/VERSION_ID; NAME/PRETTY_NAME stay CachyOS's for limine, TARGET_OS_NAME pinned), "CachyOS Linux with Steamify" in About this System; on a Steam Machine the serial number and Steam's Wi-Fi backend setting (existing installs get it as an update)**
- `07a94cd` **fix(ui): The progress screen's ✓ and ! stand upright: only a dot on the circle spins while a step runs (they kept the spinner's last angle)**
## 2.8.0 - 2026-09-28
- **chore: Version 2.8.0**
- **feat: steamify.sh --defaults --list: what --defaults can set up here, as JSON, for an installer page**
## 2.7.0 - 2026-09-28
- `4f6e6fd` **feat: --defaults takes --skip <ids> and --boot gamescope|desktop, for the Steam Machine ISO's installer pages**
- `5bf9da4` **fix: HDMI-CEC enables cec-audio-control.socket (the TV remote's volume keys); existing installs get it as an update**
- `14aa7c0` **fix: the app's progress screen says "Boot into desktop", not "Boot into Boot into"**
- `c4ec755` **feat: --defaults --options <ids> (exactly these on) replaces --skip; steamify.sh --boot gamescope|desktop on its own switches where an installed conversion starts**
- `eadbdda` **docs: release branches (release/X.Y.Z, feature/ and bugfix/ branches into it, the release into main); CI builds release/** pushes**
- `bee18b7` **docs: the agent merges feature/bugfix branches into the release branch; the release into main stays the user's**
- `501c0ba` **docs: gh or git for merging into the release branch**
- `b1adacb` **docs: gh is required for the release-branch pull requests; ask the user to install it**
- `ced4a45` **chore: the v2 app preview is gone (it is the released app): no preview pre-release step in CI, no mention in steamify-app.sh**
- **fix: turning the theme off no longer brings back single user's launcher restriction when single user was turned off in between**
## 2.6.0 - 2026-09-27
- `b948c20` **feat: --defaults applies the default setup without the menu, also from an installer (no session): the base for a Steam Machine ISO**
## 2.5.2 - 2026-09-27
- `0c96bed` **docs: README screenshot at 2.5.2: everything on, nothing to update**
## 2.5.1 - 2026-09-27
- **fix: Add as non-Steam game asks Steam to close again every 15 s (a Steam that was still starting ignored it)**
- `3a0692f` **docs: README screenshot at 2.5.1 with Add as non-Steam game**
- `e86ddcc` **feat: Add as non-Steam game: Steamify in the Steam library, under the shortcut; controller works when started from Steam**
- `389bc33` **fix(ui): B never quits (Steam sends Esc for it), Ctrl+Q does; started from Steam, the hints show the Steam Controller's buttons**
- `c6c1150` **fix(ui): Enter selects (a Steam Controller's A in Steam's desktop layout); Ctrl+Enter reviews and applies**
- `98e2eff` **fix(ui): Started from Steam, the app runs without Steam's overlay (Qt aborted) and stays in the foreground, so Steam Input gives it the controller**
- `431515b` **feat(ui): Review & apply greyed out while nothing would change; the Done screen shows why a run stopped**
## 2.5.0 - 2026-09-27
- **docs: README: what the VRAM booster says with NVIDIA's closed driver; shorter unsupported text in the app**
- `32c33e4` **fix: An option left unticked stays off: it came back ticked as "new" on the next run (since 2.3.0)**
- `2dd0f3a` **feat: VRAM booster with NVIDIA says what to do: update, switch to the open driver (chwd command), or not supported by the card**
- `57995bc` **docs: VRAM booster works with NVIDIA's open kernel modules (driver 615+); only the closed driver is greyed out**
- `d57e8d8` **docs: AGENTS.md and changelog for the vidmem detection**
- `5a34633` **feat: VRAM booster also for drivers that name their region vidmem (or numbered), NVIDIA included once its driver registers it**
- `479b70b` **feat: Everything in the home folder under steamify (state, scripts, icon), moved once from the old name**
- `2e0881b` **fix: fill takes values literally; the notifier path is quoted in its unit**
- `cce31e1` **refactor: DKMS confs in patches/; an existing leds-valve DKMS override is kept and restored**
- `bd4fa3a` **refactor: The session sync and kernel headers scripts in patches/**
- `8ffa0f2` **refactor: systemd units in services/, read with service_file like patches/**
- `8a2601d` **docs: README screenshot at 2.5.0 with Update notifications; AGENTS.md: retake it when rows change**
- `572417b` **fix: Open Steamify from the notification starts the app (own scope), the same way it was last used (app or terminal)**
- `cf73229` **feat: Update notifications: a notification and tray icon when there's a new Steamify**
## 2.4.1 - 2026-09-27
- **chore: Version 2.4.1**
- `6f598d1` **docs: README screenshot shows every option**
- `9bbe5fa` **docs: Screenshot of the app's menu in the README**
## 2.4.0 - 2026-09-27
- `03a8b8c` **chore: Version 2.4.0**
- `e699a3f` **refactor(ui): One QML file per screen; the HDMI boost's setup screens and backend commands removed**
## 2.3.0 - 2026-09-27
- `86b9744` **feat: VRAM booster greyed out with an NVIDIA card, with why**
- `b0947d7` **feat: VRAM booster only for GPUs with at least 2 GB of their own VRAM**
- `3f97c85` **feat: VRAM booster for every supported GPU, not only the Steam Machine**
- `fd162c1` **feat: New badge for default sub-options added in this version**
- `54ebddf` **feat: VRAM booster for the Steam Machine**
## 2.2.0 - 2026-09-27
- `ef585c0` **docs: Changelog entries per commit for 2.2.0**
- `32795bd` **feat(ui): Update badge on the progress screen**
- `3c8ca65` **fix(ui): Never write the sudo password to a file; Update label on the progress screen**
- The app gave `sudo` the password through a short-lived file; the helper
now reads it from a named pipe the app feeds from memory each time sudo
asks. The progress screen showed "undefined" for an update.
- `11a91ef` **feat: Update badge on components that get a newer version**
- The menu (terminal and app) shows "(update)" / an Update badge after
the name of a component a normal run will update.
- `163f790` **feat: Feature versions are Steamify versions; Steam Machine support is 2.2.0**
- Each component records the Steamify version of what it set up; when a
release changes a component, installs with an older version are ticked
and updated by a normal run ("update" in the plan and the app's
review). A setup from before counts as 2.1.0, so Steam Machine support
(2.2.0) is updated on machines set up with 2.1.0. New default
sub-options (like the power-off fix) are ticked where their parent is on.
- `e121b06` **fix: Remove whatever is left of HDMI refresh boost, pin or no pin**
- HDMI refresh boost needed the pinned kernel and is retired (newer
kernels do HDMI 2.1 themselves): whatever is left of it (saved
displays, also ones not connected, its hotplug script, the old kernel
parameter) is unticked and removed by the next run.
- `19ca966` **docs: Drop the kernel pin and HDMI refresh boost descriptions**
- `56fb908` **docs: Power-off fix texts cover every affected kernel, not just 7.2**
- `6df19eb` **feat: Power-off fix as its own sub-option, drop the kernel pin, feature versions**
- The power-off fix is a sub-option of Steam Machine support, ticked
along with it (opt-out). The kernel pin is no longer offered: an
existing pin is shown unticked, so the next run removes it and brings
back CachyOS's current kernel.
- `7694c97` **feat: Power-off fix for the Steam Machine; the kernel pin is optional**
- With recent kernels (newer 6.x, 7.0 and 7.1 updates, 7.2 and probably
later ones) the Steam Machine started again right after shutting down:
they keep the wake bit the firmware sets on GPIO pin 18, which Valve's
own kernel clears in a patch that won't reach CachyOS. A small module,
built with DKMS for each installed kernel, clears it right before
power-off. Tested with CachyOS 7.2.8: stays off, and without the module
it rebooted.
- Module sources and patches live in `patches/` instead of inside the
shell code; the single-file build and the app package include them. The
Steam Machine CEC driver change is now
`patches/cros-ec-cec-single-port.patch`.
## 2.1.0 - 2026-09-26
- **feat(ui): Manage button for HDMI refresh boost**
- The item's switch is a button: Set up… while no display is saved,
Manage after that. Manage lists the saved displays (connected or not),
removes one with its Remove button, and sets up the connected display
when it has no saved rates yet.
- **4cc87aa feat: HDMI refresh boost per display, with a Saved displays screen**
- **36d0a9d feat: HDMI refresh boost only for the display it was set up for**
- The EDID override was on the kernel command line, so any display on that
HDMI port got the first display's timings. Rates are now saved per
display and loaded at boot and at every hotplug for the display that's
connected (its ID read over DDC); a display without saved rates, or
unplugging, puts the port back on the display's own EDID. No initramfs
or boot loader change any more. An existing setup is moved over when
re-applied (`a`).
- The item shows on only while the connected display is boosted, so a new
display can be set up by ticking it; unticking removes only that one.
## 2.0.3 - 2026-09-26
- **fix: HDMI-CEC on the Steam Machine**
- Mainline's `cros_ec_cec` driver doesn't know the Steam Machine, so there
was no `/dev/cec0`. HDMI-CEC now builds Valve's version of the driver
with DKMS (`steamify-cros-ec-cec`) for every installed kernel, with a fix
so it finds amdgpu's HDMI port even though amdgpu loads first. An
existing HDMI-CEC install is ticked in the menu to add it.
## 2.0.2 - 2026-09-26
- **badfd84 docs: Non-affiliation disclaimer in the README**
- States that the project isn't affiliated with Valve or CachyOS, and that
Steam and SteamOS are Valve's trademarks.
## 2.0.1 - 2026-09-25
- **2b3f1de feat: The Steamify shortcut opens the app**
- "Steamify CachyOS" on the desktop and in the launcher starts the newest
app (in Konsole the first time, if PySide6 still has to be installed).
The terminal menu is "Steamify Terminal" in the launcher. A shortcut from
before 2.0.1 is ticked in the menu, so a normal run replaces it.
- steamify-app.sh leaves the shortcut's launcher entry alone (it has the
same id, `steamify-ui.desktop`, and always starts the newest release).
## 2.0.0 - 2026-09-25
- **feat: Steamify app (v2 UI)**
- A graphical app for controller, keyboard and TV remote, driving the same
components through `steamify.sh --backend`; the BIOS update with both
warnings; built by GitHub and run with curl.
- **feat(ui): HDMI refresh boost in the app**
- "Set up…" opens its own screen: the display, its resolution and the
rates on offer (its own mode, calculated tens, the HDMI 2.0 limit). The
display's own faster mode is switched to right away; every other rate
has a Try button (switch live, wait for the picture, then keep it within
15 s or it switches back) and a tick box. Install installs the ticked
rates. All rates are loaded when the screen opens, so a try is only a
mode switch.
- Unticking Steam Machine support or the kernel pin unticks it, as in the
menu.
- **fix(ui): The details panel scrolls instead of overflowing**
- **fix: Switching back to a rate like 59.97 Hz**
- `hdmi_set_mode` compared it as a whole number and printed an error.
## 1.2.0 - 2026-09-25
- **feat: HDMI refresh boost (Steam Machine, pinned kernel)**
- The pinned 7.1.6 kernel doesn't read the extra EDID block where monitors
list their fast modes, and does no HDMI 2.1, so HDMI displays often stay
at 60 Hz. The new menu item reads the display's EDID over DDC,
calculates the highest rate that fits HDMI 2.0 at the desktop resolution
(rounded down to ten, plus the hundred below as a safe option), tests
each step live with a confirmation, and makes the confirmed ones
permanent through `drm.edid_firmware` (Limine, sdboot-manage or GRUB).
- **feat: HDMI refresh boost and Update BIOS under Steam Machine support**
- Both are sub-options now, shown only while Steam Machine support is
ticked. Unticking it unticks them; unticking the kernel pin unticks the
HDMI refresh boost.
## 1.1.4 - 2026-09-25
- **fix: No lock screen in single user mode before a restart**
- The screen locker only read the new settings at the next login, so the
PC still locked after 5 minutes idle until then. Turning single user
mode on or off now tells it to reload them right away.
## 1.1.3 - 2026-09-25
- **fix: HDMI-CEC settings shown in Steam**
- CachyOS's gaming mode script sets `STEAM_ENABLE_CEC=0`, which hides
Steam's HDMI-CEC settings (and its CEC volume buttons). HDMI-CEC now adds
a drop-in for `steam-launcher.service` that overrides it with
`STEAM_ENABLE_CEC=1` (`/etc/steamify/steam-cec.env`).
- HDMI-CEC set up by 1.1.0-1.1.2 shows as off and ticked, so a normal run
(Enter) adds this.
## 1.1.2 - 2026-09-25 ## 1.1.2 - 2026-09-25
- **fix: Steam's HDMI-CEC settings reach cecd on a first run** - **fix: Steam's HDMI-CEC settings reach cecd on a first run**
+80 -14
View File
@@ -1,5 +1,15 @@
# Steamify CachyOS # Steamify CachyOS
The Steamify app's menu. On a Steam Machine (or any PC with an AMD or Intel
graphics card) it offers the full SteamOS conversion, on the left. On a PC with
an NVIDIA graphics card gamescope's gaming mode shows a corrupted picture, so the
menu offers **Gaming on NVIDIA** instead (Steam on the desktop, started at login,
optionally in Big Picture), on the right.
| On a Steam Machine | On a PC with an NVIDIA graphics card |
|---|---|
| ![The Steamify app's menu on a Steam Machine](assets/screenshot-menu-steam-machine.png) | ![The Steamify app's menu on a PC with an NVIDIA graphics card](assets/screenshot-menu-nvidia.png) |
Turn a CachyOS desktop PC into a SteamOS-style console: it boots straight Turn a CachyOS desktop PC into a SteamOS-style console: it boots straight
into Steam's Big Picture (gamescope), and you can switch to the KDE Plasma into Steam's Big Picture (gamescope), and you can switch to the KDE Plasma
desktop and back whenever you like - just like on a Steam Deck or Valve's desktop and back whenever you like - just like on a Steam Deck or Valve's
@@ -17,6 +27,16 @@ Steam Machine.
on the desktop too. on the desktop too.
- **Boot into: gamescope / desktop** - where the PC starts. Gaming mode is - **Boot into: gamescope / desktop** - where the PC starts. Gaming mode is
the default. the default.
- Steam's **System** settings show the SteamOS release Steamify follows
as OS Version (e.g. `steamos-3.9`), `steam-machine` as OS Codename and
Steamify's version as OS Variant. The OS name stays CachyOS
([details](TECHNICAL.md#what-steams-system-settings-show)).
- **NVIDIA graphics cards:** gamescope's gaming mode shows a corrupted
picture there (an open NVIDIA driver bug), so the conversion isn't
offered. **Gaming on NVIDIA** takes its place: Steam on the desktop,
installed if it's missing and started at login, in **Big Picture** or its
normal window as you choose
([details](TECHNICAL.md#gaming-on-nvidia)).
2. **SteamOS theme** - the Vapor look of SteamOS for the desktop, with 2. **SteamOS theme** - the Vapor look of SteamOS for the desktop, with
**Add to Steam** in right-click menus **Add to Steam** in right-click menus
([details](TECHNICAL.md#steamos-desktop-look)). ([details](TECHNICAL.md#steamos-desktop-look)).
@@ -25,14 +45,36 @@ Steam Machine.
KDE wallet password prompts (e.g. from Brave). Typing a password with a KDE wallet password prompts (e.g. from Brave). Typing a password with a
controller is no fun controller is no fun
([details](TECHNICAL.md#single-user-mode-sddm-no-locking)). ([details](TECHNICAL.md#single-user-mode-sddm-no-locking)).
5. **Steamify shortcut** - an icon on the desktop and in the launcher that 5. **Steamify shortcut** - "Steamify CachyOS" on the desktop and in the
opens the newest Steamify, so you don't need the install command again. launcher opens the newest Steamify app, so you don't need the install
6. **HDMI-CEC** (experimental; off by default, on by default on a Steam command again. "Steamify Terminal" in the launcher opens the terminal menu.
With **Add as non-Steam game** (on by default) it's in your Steam library
too: started from Steam it gets your controller (a Steam Controller only
talks to Steam), in gaming mode as well. Steam closes for a moment while
it's added, so that's done from the desktop. Not offered on PCs with an NVIDIA graphics card, which have no gaming mode.
6. **Update notifications** - on by default: a notification and a tray icon
when there's a new Steamify, with Open Steamify and Skip this version. It
never updates anything by itself
([details](TECHNICAL.md#update-notifications)).
7. **VRAM booster** - on by default: the game you're playing keeps its video
memory and background apps make room, like SteamOS 3.9. Shown for GPUs
whose driver supports it (AMD, Intel, NVIDIA with its open kernel modules
from driver 615) on kernel 7.2 or newer. With NVIDIA's closed driver it's
greyed out and says what to do: switch to the open driver when your card
supports it (RTX 20 series and newer; Steamify shows the command, it
doesn't switch drivers itself)
([details](TECHNICAL.md#vram-booster)).
8. **HDMI-CEC** (experimental; off by default, on by default on a Steam
Machine) - use Steam with your TV's Machine) - use Steam with your TV's
remote, and the TV turns on and off with the PC. Needs a PC with CEC (like remote, and the TV turns on and off with the PC. Needs a PC with CEC (like
the Steam Machine) or a USB CEC adapter; most graphics cards, NVIDIA the Steam Machine) or a USB CEC adapter; most graphics cards, NVIDIA
included, don't have it. See [CEC.md](CEC.md) for which PCs do, and how to included, don't have it. See [CEC.md](CEC.md) for which PCs do, and how to
add it to your own build ([details](TECHNICAL.md#hdmi-cec)). add it to your own build ([details](TECHNICAL.md#hdmi-cec)).
9. **Extended controller support** (off by default) - the Xbox wireless
dongle (xone) and Xbox controllers over Bluetooth with rumble, the right
button mapping and the battery level (xpadneo). Both are installed from the
CachyOS repo and built for every installed kernel
([details](TECHNICAL.md#extended-controller-support)).
## Steam Machine ## Steam Machine
@@ -42,21 +84,24 @@ What Steamify can do on a Valve Steam Machine:
TV's remote, and the TV turns on and off with the Steam Machine TV's remote, and the TV turns on and off with the Steam Machine
([details](TECHNICAL.md#hdmi-cec)). ([details](TECHNICAL.md#hdmi-cec)).
- **Steam Machine support** - the front **LED bar** works, Steam's - **Steam Machine support** - the front **LED bar** works, Steam's
**hardware settings** (fan, performance) work, and the power **hardware settings** (fan, performance) work, and the power button puts
button puts it to sleep like a console. it to sleep like a console. Steam's System settings show the serial
- **Pin the kernel to 7.1.6-1** - on by default: with newer CachyOS kernels number, and Steam's **Wi-Fi backend** setting works.
the Steam Machine reboots instead of shutting down. Untick it once CachyOS - **Power-off fix** - on by default: shutting down really turns the Steam
fixes that. The kernel comes from its own Machine off, also with the newest CachyOS kernel
[release](https://github.com/theupriser/steamify-cachyos/releases/tag/kernel-7.1.6-1), ([details](TECHNICAL.md#power-off-fix-steam-machine)).
checked against CachyOS's signature - **Update BIOS** - installs Valve's newest Steam Machine BIOS. Never ticked
([details](TECHNICAL.md#kernel-pin-steam-machine)). by default, at your own risk, and only after two warnings
- **Update BIOS** - installs Valve's newest Steam Machine BIOS. Never ticked ([details](TECHNICAL.md#bios-updates-steam-machine)).
by default, at your own risk, and only after two warnings
([details](TECHNICAL.md#bios-updates-steam-machine)).
## Requirements ## Requirements
- CachyOS with the KDE Plasma desktop (the default CachyOS Desktop edition) - CachyOS with the KDE Plasma desktop (the default CachyOS Desktop edition)
> **KDE Plasma only.** Steamify works with KDE Plasma and its login managers
> (plasma-login-manager or SDDM). Other desktops (GNOME, Hyprland, Cosmic,
> ...) and other display managers are not supported: the switching between gaming mode
> and the desktop, the theme and the shortcuts are all built for Plasma.
- Your normal user account, with permission to use `sudo` - Your normal user account, with permission to use `sudo`
## Installation ## Installation
@@ -67,6 +112,11 @@ Open **Konsole** on your Plasma desktop and run:
curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh | bash curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh | bash
``` ```
Installing CachyOS on a Steam Machine? The
[Steamify CachyOS ISO](https://github.com/theupriser/steamify-cachyos-live-iso)
has Steamify built into the installer: pick the options there, and the
first boot is already set up.
Prefer to look at the script first? Clone the repository and run Prefer to look at the script first? Clone the repository and run
[`./steamify.sh`](steamify.sh) (keep the whole folder: it needs [`./steamify.sh`](steamify.sh) (keep the whole folder: it needs
[`lib/`](lib/)). [`lib/`](lib/)).
@@ -109,6 +159,14 @@ steamos-session-select persistent # start where you left off last time
steamos-session-select oneshot # always start in gaming mode (default) steamos-session-select oneshot # always start in gaming mode (default)
``` ```
Where the PC starts after a restart can also be set from a script, once the
SteamOS conversion is on (only that changes; `sudo` may ask for your password):
```bash
steamify.sh --boot desktop # start in the desktop from the next boot on
steamify.sh --boot gamescope # start in gaming mode again (default)
```
## Something wrong? ## Something wrong?
Stuck on a black screen in gaming mode: press **Ctrl+Alt+F3**, log in, and run Stuck on a black screen in gaming mode: press **Ctrl+Alt+F3**, log in, and run
@@ -124,3 +182,11 @@ Contributing (or an AI agent)? Read [AGENTS.md](AGENTS.md).
## License ## License
MIT - do whatever you want with it. See [LICENSE.md](LICENSE.md). MIT - do whatever you want with it. See [LICENSE.md](LICENSE.md).
## Disclaimer
Steamify CachyOS is an unofficial community project. It is not affiliated
with, endorsed by, or sponsored by Valve Corporation or the CachyOS project.
Steam, SteamOS and the Steam logo are trademarks of Valve Corporation. CachyOS
is the name of its respective project. They are used here only to describe
what this script works with.
+312 -27
View File
@@ -14,7 +14,7 @@ and turns on what you ticked. To be able to undo:
off. off.
- **KDE settings** in your home directory are recorded with their previous - **KDE settings** in your home directory are recorded with their previous
value the first time the wizard changes them, in an undo journal under value the first time the wizard changes them, in an undo journal under
`~/.local/state/cachyos-gamescope-boot/`. Turning the component off writes `~/.local/state/steamify/`. Turning the component off writes
the old values back (or removes keys that didn't exist before). Setups the old values back (or removes keys that didn't exist before). Setups
made by older versions of the script, without a journal, fall back to made by older versions of the script, without a journal, fall back to
KDE's defaults. KDE's defaults.
@@ -43,6 +43,14 @@ user wallet, with anything saved in it meanwhile, is kept as
`kdewallet.kwl.steamify-single-user` and used again the next time it's on. `kdewallet.kwl.steamify-single-user` and used again the next time it's on.
Passwords aren't shared between the two wallets. Passwords aren't shared between the two wallets.
**Without the conversion (NVIDIA PCs, where gamescope's session is broken)** nothing else logs in by itself, so single user mode
does that too: `single_login_enable` (`lib/single-user.sh`) switches to SDDM and writes
`/etc/sddm.conf.d/zzz-steamify-autologin.conf` (`User=`, `Session=plasma.desktop`, `Relogin=true`; named to sort after
`steam-set-session`'s `zz-steamos-autologin.conf`, which may still say gamescope from an earlier conversion), and drops any
`[Autologin]` from `/etc/sddm.conf`. Turning it off removes the file and goes back to plasma-login-manager. With the
conversion on, the conversion does the login as before. Ticking single user mode only ticks the conversion where the
conversion is offered.
**SDDM** is what SteamOS uses, and CachyOS's `steam-set-session` supports it **SDDM** is what SteamOS uses, and CachyOS's `steam-set-session` supports it
directly: it writes `/etc/sddm.conf.d/zz-steamos-autologin.conf`, which SDDM directly: it writes `/etc/sddm.conf.d/zz-steamos-autologin.conf`, which SDDM
honours. The script installs and enables `sddm` (active from the next boot), honours. The script installs and enables `sddm` (active from the next boot),
@@ -100,9 +108,11 @@ tools write to the conf.d fragment into the base config.
(never `zz-steamos-autologin.conf`, which CachyOS's tools own), and (never `zz-steamos-autologin.conf`, which CachyOS's tools own), and
installs `/usr/local/bin/sync-steamos-session.sh` plus installs `/usr/local/bin/sync-steamos-session.sh` plus
`sync-steamos-session.path`/`.service`; `sync-steamos-session.path`/`.service`;
- `KWIN_IM_SHOW_ALWAYS=1` for the virtual keyboard and a - `KWIN_IM_SHOW_ALWAYS=1` for the virtual keyboard; the sub-option `silent`
`steam-desktop-autostart` systemd user service that starts Steam silently ("Start Steam silently in desktop mode", opt-in, always shown) writes
in Plasma only; `~/.config/autostart/steam.desktop` (the system steam.desktop with `-silent` in its Exec line). That is the
file Steam's own setting "Run Steam when my computer starts" creates and removes, so the setting shows it. An older
`steam-desktop-autostart` user unit is removed;
- the **Return to Gaming Mode** shortcut; on plasma-login-manager with a - the **Return to Gaming Mode** shortcut; on plasma-login-manager with a
narrow sudoers rule (`/etc/sudoers.d/gamescope-session-switch`) so it can narrow sudoers rule (`/etc/sudoers.d/gamescope-session-switch`) so it can
restart the login manager without a password prompt. restart the login manager without a password prompt.
@@ -111,9 +121,13 @@ Turning it off restores `/etc/plasmalogin.conf` from its backup, removes the
SDDM autologin, sync bridge, shortcut, sudoers rule and Steam autostart, and SDDM autologin, sync bridge, shortcut, sudoers rule and Steam autostart, and
switches back to plasma-login-manager. switches back to plasma-login-manager.
The conversion also sets what Steam's System settings show, see
[below](#what-steams-system-settings-show).
**Steam Deck/Machine icons:** `STEAM_GAMEPADUI_ARGS="-gamepadui -steamos3"` **Steam Deck/Machine icons:** `STEAM_GAMEPADUI_ARGS="-gamepadui -steamos3"`
in `~/.config/environment.d/` (and gamescope-session's own environment in `~/.config/environment.d/` (and gamescope-session's own environment
file), which makes Steam show Steam Deck button glyphs in gaming mode. file), which makes Steam show Steam Deck button glyphs in gaming mode. It is a sub-option of the SteamOS conversion (`gaming`):
it does nothing without gaming mode, ticking it ticks the conversion and turning the conversion off turns it off.
## SteamOS desktop look ## SteamOS desktop look
@@ -139,6 +153,33 @@ taken from the newest `steamdeck-kde-presets` on Valve's SteamOS mirror
Turning it off restores your previous look and panel layout, and removes `cachyos-vapor` again Turning it off restores your previous look and panel layout, and removes `cachyos-vapor` again
if the wizard installed it (and nothing else, like `cachyos-handheld`, needs it). if the wizard installed it (and nothing else, like `cachyos-handheld`, needs it).
## What Steam's System settings show
With the SteamOS conversion on, `/etc/os-release` gets:
| Key | Value | Shown in Steam as |
|---|---|---|
| `VERSION_ID` | `steamos-3.9`: the newest SteamOS release in Valve's repo (offline: the previous value) | OS Version |
| `VERSION_CODENAME` | `steam-machine` | OS Codename |
| `VARIANT` / `VARIANT_ID` | `Steamify 2.9.0` / `steamify-2.9.0` | OS Variant (`VARIANT_ID`) |
The OS name stays CachyOS everywhere: Steam's OS Name comes from
`lsb_release -d`, which isn't touched, and `NAME`/`PRETTY_NAME` stay
CachyOS's. limine-snapper-sync finds the boot entries by that name, so
changing it would break snapshot boot entries; `/etc/default/limine` gets
`TARGET_OS_NAME="CachyOS"` as a guard when it has none.
KDE's About this System would show `VERSION_ID` after the name
("CachyOS Linux steamos-3.9"); `UseOSReleaseVersion=true` in
`~/.config/kcm-about-distrorc` makes it show `VERSION`, which CachyOS doesn't
have, so it keeps saying "CachyOS Linux".
CachyOS's own hooks (`cachyos-hooks`) reset os-release on updates of
`filesystem`; the pacman hook `zz-steamify-os-release` runs after them and
sets the keys again (`/usr/local/libexec/steamify-os-release`). The values are
refreshed after every Steamify run that changes something. Turning the
conversion off removes the hook and runs CachyOS's `cachyos-branding` again.
## Front LED bar (Steam Machine) ## Front LED bar (Steam Machine)
The **Steam Machine support** component, only shown on Fremont hardware The **Steam Machine support** component, only shown on Fremont hardware
@@ -156,7 +197,14 @@ The **Steam Machine support** component, only shown on Fremont hardware
- installs and enables `steamos-manager`, the service Steam in gaming mode - installs and enables `steamos-manager`, the service Steam in gaming mode
uses for hardware settings (fan, performance, and the HDMI-CEC settings uses for hardware settings (fan, performance, and the HDMI-CEC settings
when [HDMI-CEC](#hdmi-cec) is on); it recognises the Steam Machine from its when [HDMI-CEC](#hdmi-cec) is on); it recognises the Steam Machine from its
DMI data. DMI data;
- makes the serial number readable for Steam, which shows it in its System
settings (`/etc/tmpfiles.d/steamify-serial.conf`: `product_serial` is
root-only by default);
- makes Steam's Wi-Fi backend setting work: steamos-manager switches it in
`/etc/NetworkManager/conf.d/99-valve-wifi-backend.conf`, which is created
with `wifi.backend=wpa_supplicant` (NetworkManager's default) when no
backend is set anywhere yet.
Turning it off removes all of that again (the AUR helper is kept). Turning it off removes all of that again (the AUR helper is kept).
@@ -169,6 +217,54 @@ or upgraded. A newly added kernel doesn't come with its headers, so
`ensure-kernel-headers.service` checks at every boot and installs any `ensure-kernel-headers.service` checks at every boot and installs any
missing `-headers` package, which makes DKMS build the driver for it. missing `-headers` package, which makes DKMS build the driver for it.
## Extended controller support
`extended_controller_support` (`lib/controllers.sh`, top-level, off by default, 2.10.0) installs two kernel drivers from CachyOS's own
repo (no AUR): `xone-dkms` with `xone-dongle-firmware` for the Xbox wireless dongle (the kernel has no driver for it; wired
Xbox controllers go through the same driver), and `xpadneo-dkms` for Xbox One/Series controllers over Bluetooth (without it
they work as plain gamepads but miss rumble including the triggers, the right button mapping and the battery level). Both are
DKMS modules, built for every installed kernel, so `install_kernel_headers` (`lib/steam-machine.sh`) makes sure each kernel has its
headers first. A PC that already has the AUR's `xone-dkms-git` keeps it: it provides the same driver and conflicts with the repo's
`xone-dkms`. Status is the three packages being installed. Off removes only what Steamify installed (state
`extended_controller_support`, `installed_pkgs`); the headers stay. The drivers load when the device shows up: the dongle is
plugged in again, or the controller reconnected. Its test is in steamify-cachyos-dev (`tests/controllers-test.sh`).
## Gaming on NVIDIA
gamescope's own gaming mode shows a corrupted picture on NVIDIA graphics cards: NVIDIA's open bug 5240452 (flicker and
artifacts in the DRM session at modes above 2560x1440@120, seen up to driver 595 and on RTX 3070 Ti, 4090, 5090 and 5080;
no fix known). The kernel parameters `nvidia-drm.modeset=1`/`fbdev=1` and loading the NVIDIA modules early don't change it
(the driver already reports both `Y`), and gamescope started nested inside KDE artifacts too when KWin hands its fullscreen
window straight to the display. What does work on an RTX 5080: Steam's Big Picture (`steam -gamepadui`) as a normal window of
the Plasma desktop session, smooth and clean.
So with an NVIDIA GPU (PCI vendor 0x10de, display class, its driver providing `nvidia_drm`: `nvidia_present`) the SteamOS
conversion (`gaming`, and its options `boot`, `single`, `glyphs`) isn't offered, unless it is already on (so it can be turned
off). `nvidia` ("Gaming on NVIDIA", `lib/nvidia.sh`) takes its place, hidden while the conversion is on (both would start
Steam at login): it installs `steam` when missing (recorded in state `nvidia`, removed again only then) and enables the user
unit `steamify-steam-autostart.service` (`services/`, `ExecStart=/usr/bin/steam @ARGS@`, Plasma only). Its sub-option
`bigpicture` ("Steam starts in Big Picture", ticked along with it) rewrites the unit with `-gamepadui` and sets
`ksmserverrc [General] loginMode=emptySession` (`kset`, undone with `krevert bigpicture`), so windows of the last session
(Discord, a browser) aren't restored on top of Big Picture; unticked, Steam starts in its normal window. The opt-in sub-option `nvsilent` ("Start Steam silently") rewrites the unit with `-silent`
(tray only); it and `bigpicture` exclude each other. The PC always boots into the desktop: there is no gamescope session. Takes effect at the next
login.
On a supported card (RTX 20 series or newer: not on chwd's legacy lists `/var/lib/chwd/ids/nvidia-*.ids`, the VRAM booster's
check, `vram_nvidia_legacy_id`; `nvidia_supported`) enabling it also does what NVIDIA's DRM stack wants, though it didn't cure
gamescope: `nvidia-drm.modeset=1 nvidia-drm.fbdev=1` on the kernel command line (Limine, systemd-boot or GRUB; the file is
backed up first; on Limine into the existing `KERNEL_CMDLINE[default]="..."` or `+="..."` line, never an extra appended line,
which ends up as text; the kernel treats `nvidia_drm` and `nvidia-drm` alike) and
`/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf` (`MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)`), then the initramfs
and boot entries are rebuilt and checked for the parameters. The early-load drop-in is only there while every installed kernel
has the NVIDIA modules (mkinitcpio fails on a missing module, and `limine-mkinitcpio` then skips that kernel's boot entry):
a pacman hook (`/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook`, after DKMS, before the initramfs is built) runs
`/usr/local/libexec/steamify-nvidia-initramfs` at every kernel or driver change and decides again. Turning it off removes
all of it. The tests are in steamify-cachyos-dev: `tests/nvidia-test.sh` (a fake GPU, a stub `pacman` and `systemctl`, a temp home) and `tests/nvidia-hardware-test.sh` (a real NVIDIA PC).
Tried and dropped (notes in steamify-cachyos-dev, `nvidia/HARDWARE-RESULTS.md`): a gamescope session that opens Big Picture in Plasma (it needed the `start-gamescope-session`
command shadowed in `/usr/local/bin`, and showed the desktop for a few seconds, then a Steam window with a black border);
a KWin session without Plasma (the mouse stuttered every second or two).
## HDMI-CEC ## HDMI-CEC
The **HDMI-CEC** item (on every PC; ticked by default only on a Steam The **HDMI-CEC** item (on every PC; ticked by default only on a Steam
@@ -180,7 +276,9 @@ SHA-256 in Valve's package index:
- `cecd`, Valve's CEC daemon (a user service started with the desktop and - `cecd`, Valve's CEC daemon (a user service started with the desktop and
gaming mode): TV remote keys become normal key presses (arrows, Enter, gaming mode): TV remote keys become normal key presses (arrows, Enter,
Back), and it turns the TV on and off with the PC; Back), and it turns the TV on and off with the PC;
- `cec-audio-control`: volume of the TV or receiver; - `cec-audio-control`: volume of the TV or receiver (the TV remote's volume
keys); its user socket is enabled by Steamify, since SteamOS enables it
through a preset that pacman doesn't apply;
- `inputattach-cec-units` (with `linuxconsole` from CachyOS for - `inputattach-cec-units` (with `linuxconsole` from CachyOS for
`inputattach`): attaches USB CEC adapters (Pulse-Eight, RainShadow). `inputattach`): attaches USB CEC adapters (Pulse-Eight, RainShadow).
@@ -192,6 +290,13 @@ devices found. Also turn on CEC on the TV
`steamos-manager` writes cecd's settings from Steam's (wake the TV, put it to `steamos-manager` writes cecd's settings from Steam's (wake the TV, put it to
sleep), in `~/.config/cecd/config.d/`, and Steam shows its HDMI-CEC sleep), in `~/.config/cecd/config.d/`, and Steam shows its HDMI-CEC
settings; the wizard restarts steamos-manager so that happens right away. settings; the wizard restarts steamos-manager so that happens right away.
CachyOS's gaming mode script (`/usr/lib/steamos/gamescope-session`) sets
`STEAM_ENABLE_CEC=0`, which hides those settings; Steam reads the script's
variables from `$XDG_RUNTIME_DIR/gamescope-environment`. HDMI-CEC adds a
drop-in for `steam-launcher.service`
(`/etc/systemd/user/steam-launcher.service.d/10-steamify-cec.conf`) with a
second `EnvironmentFile=` (`/etc/steamify/steam-cec.env`,
`STEAM_ENABLE_CEC=1`), which overrides it from the next gaming mode start.
It's experimental: on SteamOS itself CEC can wake the Steam Machine right It's experimental: on SteamOS itself CEC can wake the Steam Machine right
after it goes to sleep (Samsung TVs, after it goes to sleep (Samsung TVs,
@@ -201,26 +306,144 @@ upset CEC for the TV's other devices
Turning it off removes the packages again (`linuxconsole` only if the Turning it off removes the packages again (`linuxconsole` only if the
wizard installed it). wizard installed it).
## Kernel pin (Steam Machine) ## Power-off fix (Steam Machine)
With CachyOS kernels newer than 7.1.6 a Steam Machine reboots instead of With recent kernels a Steam Machine starts again right after powering off:
shutting down. The **Pin the kernel** sub-option (ticked along with Steam the newer 6.x, 7.0 and 7.1 updates and 7.2 (and probably every kernel after
Machine support) installs `linux-cachyos` and `linux-cachyos-headers` it). Linux stopped clearing the S4/S5 wake bits at probe
7.1.6-1 and adds them to `IgnorePkg` in `/etc/pacman.conf`, so updates skip (`pinctrl-amd: Don't clear S4 wake bits at probe`, in 7.2 and backported to
them. DKMS builds the LED driver for it; restart to boot it. stable kernels), and the firmware leaves
that bit set on GPIO pin 18 (`_SB.PCI0.GPP6`). Valve's own kernel
(`linux-neptune-72`) clears it at probe on Fremont, in a patch marked not
for upstream ("until the firmware is fixed"), so CachyOS and mainline won't
get it.
The packages (and their signatures, which pacman checks) are kept in The **Power-off fix** sub-option of Steam Machine support (ticked along with
`/var/cache/steamify/kernel`, so re-applying needs no download. Missing it, can be unticked) builds a small module with DKMS for every installed
files are taken from pacman's cache, else downloaded from this repo's kernel, `steamify-fremont-poweroff` (source in
`kernel-7.1.6-1` release, then `archive.cachyos.org`, then [`patches/steamify-fremont-poweroff.c`](patches/steamify-fremont-poweroff.c)).
`mirror.cachyos.org` (which only has the current kernel). Every file must It only loads on Fremont (DMI board name) and touches one register: right
match the SHA-256 in the script and have a valid CachyOS signature; a bad before power-off (a `SYS_OFF_MODE_POWER_OFF_PREPARE` handler, after the
one is deleted, so the next run downloads it again. Set `PINNED_KERNEL_URL` drivers have shut down) it clears that pin's S4/S5 wake bit. On a kernel
to a directory URL with the files to try another source first, or drop them that already clears it, it does nothing. It logs the pin's state at load:
into the kernel directory yourself. `dmesg | grep 'GPIO 18'`; `/sys/kernel/debug/gpio` shows the S4/S5 column.
Tested on a Steam Machine with `linux-cachyos-bore` 7.2.8: it stayed off
three times in a row, and rebooted right away with the module unloaded.
Unticking it removes the pin and runs `sudo pacman -Syu`, which brings the Where Steam Machine support was set up before 2.2.0, the menu ticks the
kernel back to CachyOS's current version; the files stay for next time. power-off fix as a new default sub-option (see feature versions below), so a
normal run adds it.
## Add as non-Steam game
A sub-option of the Steamify shortcut (`steamgame`, `lib/steam-game.sh`, new
in 2.5.1, ticked along with the shortcut): Steamify CachyOS in every Steam
account's non-Steam games (`~/.local/share/Steam/userdata/<id>/config/shortcuts.vdf`,
binary VDF, edited by `patches/steam-shortcuts.py`), starting the shortcut's
`run-app`, with the icon rendered as PNG (`~/.local/share/steamify/steamify.png`;
Steam shows no SVG). Why: while Steam runs, a Steam Controller only talks to
Steam, which gives its input to the game it started (Steam Input); on the
desktop through its desktop layout (A = Enter, B = Esc, Y = Space, X =
Steam's keyboard), in gaming mode as a gamepad.
Steam reads the list only at startup and writes it back on exit, so Steam is
closed (`steam -shutdown`) for the edit and started again (`steam -silent`). That's refused in gaming
mode and when Steamify itself was started from Steam (`SteamGameId`), since
closing Steam would end it. An entry for the start script that's already
there (added by hand, or with the path from before 2.5.0) is updated, not
added twice; what was done is recorded per account (state `steamgame`:
`<account>=<appid> added|updated`) and a copy of each list is kept in the
state directory before the first change. Turning it off (or the shortcut)
removes every entry for the start script, since it can't start anything
without it.
Started from Steam, the app re-executes itself without Steam's overlay
(`LD_PRELOAD` of `gameoverlayrenderer.so` makes Qt abort) and
`steamify-app.sh` keeps it in the foreground (`SteamGameId`), or Steam
counts the game as ended right away and stops giving it the controller.
## Update notifications
The **Update notifications** option (new in 2.5.0, ticked by default)
installs `patches/steamify-notifier.py` as
`~/.local/share/steamify/bin/steamify-notifier` with two user
units in `~/.config/systemd/user`: `steamify-update-check.timer` (daily,
`Persistent=`) and `steamify-update-check.service` (also wanted by
`plasma-workspace.target`, so it checks a minute after each desktop login).
Nothing stays running while there's no update.
Every Steamify run records its version as `seen` (`notify.state`). The check
asks GitHub for the newest release; when it's newer than `seen` and not the
`skipped` version, it shows a notification (`notify-send` with the buttons
**Open Steamify** and **Skip this version**) and a tray icon (click: open;
menu: open, skip, remind me later). Closing the notification keeps the
icon; opening Steamify in any way records the new version and ends it. It
only runs on the Plasma desktop: gamescope shows no notifications or tray,
so an update found in gaming mode waits for the next desktop login. It never
installs anything: the shortcut always starts the newest release, which then
shows what's new or updated for this install.
## VRAM booster
SteamOS 3.9 manages the dGPU's VRAM per cgroup: the game in front is
protected, background apps are evicted first. Without it a game that needs
most of the VRAM (8 GB on the Steam Machine) can be pushed into system RAM
by the desktop and other apps, and stutter. The kernel side is the `dmem`
cgroup controller (7.2); amdgpu and Intel's xe register their VRAM with it
(`drm/<pci>/vram`), NVIDIA's open kernel modules from driver 615 too
(`nvidia/<pci>/vidmem`); `dmemcg-booster` protects every region it lists,
whatever its name. NVIDIA's closed modules don't: with such a card the option is shown greyed
out with what to do (`vram_nvidia_case`): open modules older than 615 ->
update; the closed driver on a card the open one supports (not in chwd's
legacy lists `/var/lib/chwd/ids/nvidia-{580,470,390}.ids`) -> the `chwd`
commands to switch, never switched by Steamify itself (a failed switch
means a black screen, and it can't be tested here); a legacy card or
nouveau -> not supported. Shown greyed
out, with why, until its driver lists a region: then it's offered like any
other (`WIZARD_VRAM_FAKE_NVIDIA=1` fakes the grey-out, `WIZARD_VRAM_CAPACITY=<file>`
reads the regions from a copy, for tests). Otherwise
the **VRAM booster** option is only shown when
`/sys/fs/cgroup/dmem.capacity` lists a VRAM region (`vram`, or `vidmem`
as other drivers name it, numbered with several) of at least 2 GB (a
dedicated GPU; an integrated one registers a small carve-out) or it's already on, and is
ticked by default; new in 2.3.0, so setups with the SteamOS conversion on
get it ticked. CachyOS packages the userspace side, which it installs:
- `dmemcg-booster`: `dmemcg-booster-system.service` enables the `dmem`
controller and sets `dmem.low` for the cgroup in front;
`dmemcg-booster-user.service` runs in the user session.
- `plasma-foreground-booster`: tells it which window is in front on the
desktop. It only starts with `autostart=true` under `[Foreground Booster]`
in `kcgroupsrc`, which the wizard sets (and reverts).
Check it with `cat /sys/fs/cgroup/cgroup.subtree_control` (lists `dmem`) and
the `dmem.low` of the slices under `user@<uid>.service`. Turning it off
disables the services and removes the packages the wizard installed.
Not added: a fixed `dmem.max` cap for `app.slice` (as some guides do); only
worth it if the booster itself turns out to cause stutter.
## Feature versions (updates)
Each component has a feature version (`FEATURE_VERSION` in `lib/menu.sh`):
the Steamify version in which what it sets up last changed (2.1.0 for
everything that hasn't changed since). After a component is turned on
successfully, that version is recorded in `~/.local/state/steamify/features.state`
(turning it off records `off`). Whether a component is on is always checked
on the system itself; the version only decides about updates:
- **On, recorded version older** than the current one: ticked and
re-applied by a normal run; the plan (and the app's review) says
"update". A setup from before versions were recorded counts as 2.1.0,
so on a machine set up with 2.1.0, Steam Machine support (2.2.0) is
updated.
- **A new default option** (not in `NO_PRESELECT`) whose parent is on (for
a top-level one: the SteamOS conversion) and that was never turned on or
off: ticked, so a normal run adds it. Turning it off once records `off`,
so it isn't ticked again. The menu and the app show it with a **New**
badge, and the plan says "new in this version".
Set a component's entry to the new `VERSION` whenever what its
`<id>_enable` sets up changes.
## BIOS updates (Steam Machine) ## BIOS updates (Steam Machine)
@@ -258,6 +481,32 @@ To walk through it without flashing anything, run the wizard with
`WIZARD_BIOS_DRY_RUN=1`: it downloads and checks the package and shows both `WIZARD_BIOS_DRY_RUN=1`: it downloads and checks the package and shows both
warnings, skips fwupd's device check, and only prints the install command. warnings, skips fwupd's device check, and only prints the install command.
## Where Steamify keeps its files
In the home folder, everything is under `steamify`:
| Where | What |
|---|---|
| `~/.local/state/steamify/` | State (`*.state`), undo journals (`*.journal`), the saved panel layout (`theme-layout/`) |
| `~/.local/share/steamify/app/` | The app (`steamify-app.sh` downloads it here) |
| `~/.local/share/steamify/bin/` | The shortcut's start scripts (`run-app`, `run-wizard`) and `steamify-notifier` |
| `~/.local/share/icons/hicolor/scalable/apps/steamify.svg` | The shortcut's icon |
| `~/.local/share/applications/steamify-*.desktop` | The launcher entries |
| `~/.config/autostart/steam.desktop` | the `silent` option (Steam's "Run Steam when my computer starts") |
| `~/.config/systemd/user/` | `steamify-update-check.*` |
System-wide: `/usr/local/lib/steamify/` (kernel headers script),
`/usr/src/steamify-*` (DKMS modules), `/var/cache/steamify/kernel` (the old
kernel pin's packages), and `*.bak-gamescope-wizard` backups next to the
system files that were edited (that name stays: it's how they're found again).
Before 2.5.0 the home folder paths used the project's old name
(`cachyos-gamescope-boot`). `migrate_layout` (`lib/state.sh`) moves them at
every start of a newer version, once, and leaves symlinks under the old names
so an older release still finds the same state; it fixes the paths in the
launcher entries and the update check's unit, and merges when both exist.
The headers script moves when Steam Machine support is set up again.
## Manual session control ## Manual session control
```bash ```bash
@@ -267,6 +516,33 @@ steamos-session-select persistent # remember the last-used session across reboo
steamos-session-select oneshot # always start in gamescope (default, like a Deck) steamos-session-select oneshot # always start in gamescope (default, like a Deck)
``` ```
`steamify.sh --boot gamescope|desktop` sets where the PC starts after a
restart (the menu's Boot into row) from a script: it needs the SteamOS
conversion and changes nothing else, not even the updates a normal run would
apply. Exit code 1 without the conversion or with another value.
## Install-time mode (`--defaults`)
For an installer (the Steam Machine ISO), without a session or prompts
(passwordless sudo required):
```bash
steamify.sh --defaults # what the menu would preselect
steamify.sh --defaults --options gaming,theme,cec,machine --boot desktop
steamify.sh --defaults --list # what --defaults can set up here, as JSON
```
`--options` turns on exactly the listed items (the menu's ids: `gaming`,
`theme`, `glyphs`, `single`, `launcher`, `notify`, `vram`, `cec`, `machine`,
`poweroff`), everything else off; an item that's on brings its parent
(single user the conversion, the power-off fix Steam Machine support). An
item this PC can't use (e.g. `vram` without a VRAM region) is left off with
a warning. `--boot desktop` needs `gaming`. Unknown options, ids, `boot` or
`bios` in the list: exit code 1 before anything changes. What needs a
Plasma session waits for the first login (`lib/first-login.sh`). The same step (and the online theme path) replaces Plasma's default taskbar pins, which include Discover
(not installed on CachyOS: a blank icon), by CachyOS's own (`taskbar_drop_discover` in `lib/vapor-theme.sh`; skipped when Discover is
installed or the taskbar already has its own `launchers`).
With `Relogin=true`, a gamescope session that fails to start is restarted With `Relogin=true`, a gamescope session that fails to start is restarted
immediately, which can turn into a loop - see immediately, which can turn into a loop - see
[TROUBLESHOOTING.md](TROUBLESHOOTING.md) for the way out. [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for the way out.
@@ -290,9 +566,18 @@ immediately, which can turn into a loop - see
| `lib/wizard-shortcut.sh` | Steamify shortcut: desktop icon and launcher entry that run the newest release | | `lib/wizard-shortcut.sh` | Steamify shortcut: desktop icon and launcher entry that run the newest release |
| `lib/bios.sh` | Update BIOS (Steam Machine, opt-in): current/newest version, double confirmation, fwupd | | `lib/bios.sh` | Update BIOS (Steam Machine, opt-in): current/newest version, double confirmation, fwupd |
| `lib/cec.sh` | HDMI-CEC: Valve's `cecd` and friends from its `holo` repository | | `lib/cec.sh` | HDMI-CEC: Valve's `cecd` and friends from its `holo` repository |
| `lib/steam-machine.sh` | Steam Machine support: LED driver, LED access, steamos-manager; kernel pin | | `lib/steam-machine.sh` | Steam Machine support: LED driver, LED access, steamos-manager |
| `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`) | | `lib/fremont-poweroff.sh` | Steam Machine support: the power-off fix (DKMS module from `patches/`) |
| `.github/workflows/bundle.yml` | Builds and checks it on every push; publishes it on `main` | | `lib/steam-game.sh` | Add as non-Steam game: Steamify in the Steam library (`patches/steam-shortcuts.py`) |
| `lib/update-notifier.sh` | Update notifications: the notifier from `patches/` and its user timer |
| `lib/first-login.sh` | `--defaults` without a session (the Steam Machine ISO's installer): the one-time first-login step that sets up single user's launcher on Plasma's new layout and opens the app |
| `lib/controllers.sh` | Extended controller support: Xbox wireless dongle (xone) and Xbox Bluetooth (xpadneo) from the CachyOS repo |
| `lib/nvidia.sh` | Gaming on NVIDIA: Steam on the desktop, started at login, optionally in Big Picture (replaces the SteamOS conversion there) |
| `lib/vram-booster.sh` | VRAM booster (`dmemcg-booster`, `plasma-foreground-booster`) |
| `services/` | The systemd units the scripts install (`service_file`, `@KEY@` placeholders); see its README |
| `patches/` | Module sources and patches the scripts build or apply (`patch_file`); see its README |
| `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`), with `patches/` and `services/` embedded |
| `.github/workflows/bundle.yml` | Builds and checks it on every push (GitHub and a Gitea mirror); publishes it on `main`, and starts the Steamify ISO's release |
The single-file version is generated: on every push to `main`, GitHub The single-file version is generated: on every push to `main`, GitHub
Actions runs `.github/tools/bundle.sh`, checks the result with `bash -n` and Actions runs `.github/tools/bundle.sh`, checks the result with `bash -n` and
+8
View File
@@ -18,6 +18,14 @@ restarted right away (`Relogin=true`), which is why it can loop.
Run the wizard again and press `a` in the menu to re-apply everything that's Run the wizard again and press `a` in the menu to re-apply everything that's
on (also useful after a CachyOS update). To undo a part, untick it. on (also useful after a CachyOS update). To undo a part, untick it.
## "Target OS name ... not found in /boot/limine.conf"
limine-snapper-sync finds the boot entries by the OS name. Steamify never
changes that name (it only adds version keys to `/etc/os-release`), and pins
`TARGET_OS_NAME="CachyOS"` in `/etc/default/limine`. If you see this message,
check that `PRETTY_NAME` in `/etc/os-release` still says `CachyOS`, or set
`TARGET_OS_NAME` in `/etc/default/limine` to the name your boot entries use.
## The LED bar on the Steam Machine stays dark ## The LED bar on the Steam Machine stays dark
Restart once more; a freshly installed driver often needs a reboot. The menu Restart once more; a freshly installed driver often needs a reboot. The menu
Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 KiB

+205
View File
@@ -0,0 +1,205 @@
#!/bin/bash
# Machine-readable mode for the graphical app (steamify-ui): the same
# components and plan as the menu, as JSON on stdout.
# steamify.sh --backend status
# one JSON object: version, machine, and every menu item with its
# current state and what a run would pick by default.
# steamify.sh --backend apply [--reapply] [--boot gamescope|desktop] <id>...
# turns on the listed components and off the others (like ticking them
# in the menu), then streams one JSON object per line: plan, start,
# log, done, finished. sudo asks through $SUDO_ASKPASS (no terminal).
# steamify.sh --backend bios-prepare | bios-flash
# the BIOS update in two steps, so the app shows both warnings between
# them (bios-ready event: current, newest, checksum, compatible).
# Sourced by steamify.sh; not meant to be run on its own.
json_str() {
# json_str <text>: <text> as a JSON string (quotes, backslashes and
# control characters escaped; colour codes dropped).
local s="$1"
s="$(printf '%s' "$s" | sed -E $'s/\x1b\\[[0-9;]*[A-Za-z]//g')"
s="${s//\\/\\\\}"; s="${s//\"/\\\"}"
s="${s//$'\t'/\\t}"; s="${s//$'\r'/}"; s="${s//$'\n'/\\n}"
s="$(printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037')"
printf '"%s"' "$s"
}
json_list() {
# json_list <item>...: a JSON array of strings.
local out="" x
for x in "$@"; do out+="${out:+,}$(json_str "$x")"; done
printf '[%s]' "$out"
}
backend_event() {
# backend_event <event> [<json fields without braces>]
printf '{"event":%s%s}\n' "$(json_str "$1")" "${2:+,$2}"
}
backend_status() {
local c first_run=true items="" kind parent now
detect_components
for c in "${COMPONENTS[@]}"; do
component_available "$c" && ! is_action "$c" && [[ "${CURRENT[$c]}" == 1 ]] && first_run=false
done
for c in "${COMPONENTS[@]}"; do
component_available "$c" || continue
kind=toggle; is_action "$c" && kind=action; [[ "$c" == boot ]] && kind=choice
parent="${PARENT[$c]:-}"
now="${CURRENT[$c]:-0}"
items+="${items:+,}{\"id\":$(json_str "$c"),\"label\":$(json_str "${LABEL[$c]%%:*}")"
items+=",\"hint\":$(json_str "$( [[ "${LABEL[$c]}" == *:* ]] && echo "${LABEL[$c]#*: }")")"
items+=",\"kind\":\"$kind\",\"parent\":$(json_str "$parent")"
items+=",\"needs\":\"\""
items+=",\"on\":$( [[ "$now" == 1 ]] && echo true || echo false)"
items+=",\"wanted\":$( [[ "${WANTED[$c]:-0}" == 1 ]] && echo true || echo false)"
items+=",\"update\":$(feature_outdated "$c" && echo true || echo false)"
items+=",\"new\":$({ feature_new "$c" || feature_new_optin "$c"; } && echo true || echo false)"
items+=",\"selectable\":$(component_selectable "$c" && echo true || echo false)"
# Why it can't be turned on, for the app's explanation.
[[ "$c" == vram && -n "${VRAM_NVIDIA_CASE:-}" ]] &&
items+=",\"note\":$(json_str "$(vram_nvidia_note "$VRAM_NVIDIA_CASE")")"
items+="}"
done
local cec="" f
for f in /dev/cec*; do [[ -e "$f" ]] && cec+="${cec:+ }$(basename "$f")"; done
local bios='null'
if bios_available; then
bios="{\"current\":$(json_str "$(bios_current)"),\"newest\":$(json_str "${BIOS_NEWEST:-}"),\"selectable\":$(bios_selectable && echo true || echo false),\"dryRun\":$([[ -n "$BIOS_DRY_RUN" ]] && echo true || echo false)}"
fi
printf '{"version":%s,"firstRun":%s,"steamMachine":%s,"kernel":%s,"pinnedKernel":%s,"cecDevices":%s,"leds":%s,"bios":%s,"items":[%s]}\n' \
"$(json_str "$VERSION")" "$first_run" \
"$(detect_valve_fremont && echo true || echo false)" \
"$(json_str "$(uname -r)")" "$(json_str "${PINNED_KERNEL_VER:-}")" \
"$(json_str "$cec")" \
"$(compgen -G '/sys/class/leds/valve-leds*' | wc -l)" \
"$bios" "$items"
}
backend_run_component() {
# backend_run_component <id> <enable|disable>: runs it in this shell
# (its variables, e.g. RESTART_FOR_LOGIN, must survive), each output line
# as a log event.
local c="$1" what="$2" rc
backend_event start "\"id\":$(json_str "$c"),\"action\":\"$what\",\"label\":$(json_str "${LABEL[$c]%%:*}")"
"${c}_${what}" > >(while IFS= read -r line; do backend_event log "\"id\":$(json_str "$c"),\"line\":$(json_str "$line")"; done) 2>&1
rc=$?
# Let the log reader finish before the next event.
wait $! 2>/dev/null
backend_event "done" "\"id\":$(json_str "$c"),\"ok\":$([[ $rc -eq 0 ]] && echo true || echo false)"
return $rc
}
backend_sudo() {
# sudo from the app has no terminal: ask through its helper, and keep the
# credentials fresh as the menu does.
if [[ -z "${SUDO_ASKPASS:-}" ]] && ! sudo -n true 2>/dev/null; then
backend_event finished '"failed":[],"restart":false,"error":"no-sudo"'
return 1
fi
sudo() { command sudo ${SUDO_ASKPASS:+-A} "$@"; }
if ! sudo -n true 2>/dev/null && ! sudo true 2>/dev/null; then
backend_event finished '"failed":[],"restart":false,"error":"wrong-password"'
return 1
fi
while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null &
}
backend_bios_prepare() {
# Download, checksum and fwupd's device check; the app then shows its
# two warnings and calls bios-flash.
local rc
backend_sudo || return 1
backend_event start '"id":"bios","action":"prepare","label":"Update BIOS"'
bios_prepare > >(while IFS= read -r line; do backend_event log "\"id\":\"bios\",\"line\":$(json_str "$line")"; done) 2>&1
rc=$?; wait $! 2>/dev/null
case $rc in
0) backend_event bios-ready "\"current\":$(json_str "$(bios_current)"),\"newest\":$(json_str "$BIOS_NEWEST"),\"checksum\":true,\"compatible\":$(json_str "$BIOS_COMPATIBLE")" ;;
2) backend_event finished '"failed":[],"restart":false,"nothing":true' ;;
*) backend_event finished '"failed":["bios"],"restart":false' ;;
esac
}
backend_bios_flash() {
backend_sudo || return 1
local rc
backend_run_component bios flash_only; rc=$?
backend_event finished "\"failed\":$([[ $rc -eq 0 ]] && echo '[]' || echo '["bios"]'),\"restart\":$(restart_needed && echo true || echo false),\"bios\":true"
}
backend_apply() {
local reapply=false boot="" id c
local -a want=()
while [[ $# -gt 0 ]]; do
case "$1" in
--reapply) reapply=true ;;
--boot) boot="$2"; shift ;;
*) want+=("$1") ;;
esac
shift
done
detect_components
# What the app ticked: exactly these on, every other component off.
for c in "${COMPONENTS[@]}"; do
component_available "$c" || continue
is_action "$c" && { WANTED[$c]=0; continue; }
WANTED[$c]=0
for id in "${want[@]}"; do [[ "$id" == "$c" ]] && WANTED[$c]=1; done
done
# The same rules as the menu's toggles.
# (Single user mode only needs the conversion where it is offered: not on NVIDIA PCs.)
[[ "${WANTED[single]:-0}" == 1 ]] && component_available gaming && WANTED[gaming]=1
if component_available gaming; then
[[ "${WANTED[gaming]:-0}" == 0 ]] && { WANTED[single]=0; WANTED[boot]=0; }
else
WANTED[boot]=0
fi
[[ "${WANTED[bigpicture]:-0}" == 1 ]] && WANTED[nvidia]=1
[[ "${WANTED[nvsilent]:-0}" == 1 ]] && WANTED[nvidia]=1
[[ "${WANTED[bigpicture]:-0}" == 1 ]] && WANTED[nvsilent]=0
[[ "${WANTED[nvidia]:-0}" == 0 ]] && { WANTED[bigpicture]=0; WANTED[nvsilent]=0; }
[[ "${WANTED[silent]:-0}" == 1 || "${WANTED[glyphs]:-0}" == 1 ]] && component_available gaming && WANTED[gaming]=1
[[ "${WANTED[gaming]:-0}" == 0 ]] && { WANTED[silent]=0; WANTED[glyphs]=0; }
[[ "${WANTED[poweroff]:-0}" == 1 ]] && WANTED[machine]=1
[[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[poweroff]=0
[[ "${WANTED[kpin]:-0}" == 1 ]] && WANTED[machine]=1
[[ "${WANTED[machine]:-0}" == 0 ]] && WANTED[kpin]=0
# Retired: only ever removed.
WANTED[hdmi]=0
case "$boot" in desktop) WANTED[boot]=1; WANTED[gaming]=1 ;; gamescope) WANTED[boot]=0 ;; esac
REAPPLY=$reapply
plan_changes
backend_event plan "\"disable\":$(json_list "${TO_DISABLE[@]}"),\"enable\":$(json_list "${TO_ENABLE[@]}")"
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
feature_record_unticked
backend_event finished '"failed":[],"restart":false,"nothing":true'
return 0
fi
backend_sudo || return 1
LOGIN_MANAGER=plasmalogin
[[ "${WANTED[single]}" == 1 ]] && LOGIN_MANAGER=sddm
local -a failed=()
for c in "${TO_DISABLE[@]}"; do
if backend_run_component "$c" disable; then feature_record "$c" disable; else failed+=("$c"); fi
done
for c in "${TO_ENABLE[@]}"; do
if backend_run_component "$c" enable; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi
done
feature_record_unticked
os_version_refresh
[[ " ${TO_DISABLE[*]} ${TO_ENABLE[*]} " =~ \ (gaming|single|boot|kpin)\ ]] && RESTART_FOR_LOGIN=true
backend_event finished "\"failed\":$(json_list "${failed[@]}"),\"restart\":$(restart_needed && echo true || echo false)"
}
backend_main() {
case "${1:-}" in
status) backend_status ;;
apply) shift; backend_apply "$@" ;;
bios-prepare) backend_bios_prepare ;;
bios-flash) backend_bios_flash ;;
*) err "Usage: steamify.sh --backend status | apply [--reapply] [--boot gamescope|desktop] <id>..."; return 2 ;;
esac
}
+74 -44
View File
@@ -121,54 +121,102 @@ bios_fits_device() {
grep -q '"Guid"' <<< "$details" && ! grep -q '"UpdateError"' <<< "$details" grep -q '"Guid"' <<< "$details" && ! grep -q '"UpdateError"' <<< "$details"
} }
bios_enable() { # The checked firmware waits here between bios_prepare and bios_flash, so the
# app can show its warnings in between (and the terminal flow uses it too).
BIOS_STAGE_DIR="${XDG_RUNTIME_DIR:-/tmp}/steamify-bios"
bios_prepare() {
# Downloads Valve's package, checks its SHA-256 and extracts the .cab to
# $BIOS_STAGE_DIR, then asks fwupd whether it fits this machine. Sets
# BIOS_COMPATIBLE (yes|dry-run). 0 = ready to flash, 2 = nothing to do
# (already the newest), 1 = error (nothing was touched).
if ! bios_lookup_newest; then if ! bios_lookup_newest; then
err "Couldn't find the newest Steam Machine BIOS on Valve's mirror ($VALVE_MIRROR)." err "Couldn't find the newest Steam Machine BIOS on Valve's mirror ($VALVE_MIRROR)."
return 1 return 1
fi fi
local current tmp compatible
current="$(bios_current)"
[[ -n "$BIOS_DRY_RUN" ]] && warn "DRY RUN (WIZARD_BIOS_DRY_RUN): nothing will be flashed." [[ -n "$BIOS_DRY_RUN" ]] && warn "DRY RUN (WIZARD_BIOS_DRY_RUN): nothing will be flashed."
if [[ "$current" == "$BIOS_NEWEST" ]]; then if [[ "$(bios_current)" == "$BIOS_NEWEST" ]]; then
ok "The BIOS is already the newest version ($current); nothing to do." ok "The BIOS is already the newest version ($BIOS_NEWEST); nothing to do."
return 0 return 2
fi fi
# Download and check everything before asking: the warnings only come
# when the file is Valve's (SHA-256 from Valve's repo database) and fwupd
# confirms it's firmware for this very machine.
pacman -Q fwupd >/dev/null 2>&1 || sudo pacman -S --needed --noconfirm fwupd || pacman -Q fwupd >/dev/null 2>&1 || sudo pacman -S --needed --noconfirm fwupd ||
{ err "Installing fwupd failed."; return 1; } { err "Installing fwupd failed."; return 1; }
tmp="$(mktemp -d)" rm -rf "$BIOS_STAGE_DIR"; mkdir -p "$BIOS_STAGE_DIR"; chmod 700 "$BIOS_STAGE_DIR"
info "Downloading $BIOS_PKG ($BIOS_REPO)..." info "Downloading $BIOS_PKG ($BIOS_REPO)..."
if ! curl -fsSL "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_PKG" -o "$tmp/pkg.tar.zst" || if ! curl -fsSL "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_PKG" -o "$BIOS_STAGE_DIR/pkg.tar.zst" ||
! echo "$BIOS_SHA256 $tmp/pkg.tar.zst" | sha256sum -c --quiet - || ! echo "$BIOS_SHA256 $BIOS_STAGE_DIR/pkg.tar.zst" | sha256sum -c --quiet - ||
! tar -I unzstd -xf "$tmp/pkg.tar.zst" -C "$tmp" "$BIOS_CAB"; then ! tar -I unzstd -xf "$BIOS_STAGE_DIR/pkg.tar.zst" -C "$BIOS_STAGE_DIR" "$BIOS_CAB"; then
err "Downloading or verifying $BIOS_PKG failed; the BIOS was not touched." err "Downloading or verifying $BIOS_PKG failed; the BIOS was not touched."
rm -rf "$tmp" rm -rf "$BIOS_STAGE_DIR"
return 1 return 1
fi fi
ok "Checksum OK: this is Valve's $BIOS_PKG." ok "Checksum OK: this is Valve's $BIOS_PKG."
local yes="$c_green$c_bold" b="$c_bold" n="$c_reset"
if [[ -n "$BIOS_DRY_RUN" ]]; then if [[ -n "$BIOS_DRY_RUN" ]]; then
warn "Dry run: skipping fwupd's check that the firmware fits this machine." warn "Dry run: skipping fwupd's check that the firmware fits this machine."
compatible="${c_yellow}${b}not checked${n} (dry run)" BIOS_COMPATIBLE=dry-run
elif bios_fits_device "$tmp/$BIOS_CAB"; then elif bios_fits_device "$BIOS_STAGE_DIR/$BIOS_CAB"; then
ok "fwupd confirms BIOS $BIOS_NEWEST is firmware for this machine." ok "fwupd confirms BIOS $BIOS_NEWEST is firmware for this machine."
compatible="${yes}yes${n} (checked by fwupd)" BIOS_COMPATIBLE=yes
else else
err "fwupd says BIOS $BIOS_NEWEST is not for this machine's hardware; not installing it." err "fwupd says BIOS $BIOS_NEWEST is not for this machine's hardware; not installing it."
rm -rf "$tmp" rm -rf "$BIOS_STAGE_DIR"
return 1 return 1
fi fi
printf '%s\n' "$BIOS_NEWEST" "$BIOS_CAB" > "$BIOS_STAGE_DIR/ready"
}
bios_flash() {
# Hands the firmware bios_prepare checked to fwupd; it is written during
# the next restart. Sets BIOS_NEEDS_RESTART.
local newest cab
{ read -r newest; read -r cab; } < "$BIOS_STAGE_DIR/ready" 2>/dev/null
if [[ -z "${cab:-}" || ! -f "$BIOS_STAGE_DIR/$cab" ]]; then
err "No checked BIOS file waiting; nothing was flashed."
return 1
fi
if [[ -n "$BIOS_DRY_RUN" ]]; then
ok "Dry run: would run: fwupdmgr install -y --no-reboot-check $(basename "$cab")"
ok "Dry run finished; nothing was flashed."
# Treated as staged, so the restart choices that follow a real
# update show up too; restarting only prints (see restart_now).
BIOS_NEEDS_RESTART=1
rm -rf "$BIOS_STAGE_DIR"
return 0
fi
info "Handing BIOS $newest to fwupd. Do NOT turn off the power from now on."
# -y: the user confirmed twice; --no-reboot-check: our own restart
# question comes at the end.
if ! sudo fwupdmgr install -y --no-reboot-check "$BIOS_STAGE_DIR/$cab"; then
err "fwupd could not install the BIOS update (see above); the BIOS was not changed."
rm -rf "$BIOS_STAGE_DIR"
return 1
fi
rm -rf "$BIOS_STAGE_DIR"
BIOS_NEEDS_RESTART=1
ok "BIOS $newest is staged. It is written during the next restart:"
warn "keep the power on and don't touch the machine until it has fully started again."
}
bios_enable() {
local rc current compatible
current="$(bios_current)"
bios_prepare; rc=$?
[[ $rc -eq 2 ]] && return 0
[[ $rc -ne 0 ]] && return 1
local yes="$c_green$c_bold" b="$c_bold" n="$c_reset"
if [[ "$BIOS_COMPATIBLE" == dry-run ]]; then
compatible="${c_yellow}${b}not checked${n} (dry run)"
else
compatible="${yes}yes${n} (checked by fwupd)"
fi
bios_disclaimer "WARNING: BIOS UPDATE - ENTIRELY AT YOUR OWN RISK" \ bios_disclaimer "WARNING: BIOS UPDATE - ENTIRELY AT YOUR OWN RISK" \
"Current BIOS: ${b}$current${n}" \ "Current BIOS: ${b}$current${n}" \
"New BIOS: ${b}$BIOS_NEWEST${n}" \ "New BIOS: ${b}$BIOS_NEWEST${n}" \
"Checksum: ${yes}OK${n} (Valve's package)" \ "Checksum: ${yes}OK${n} (Valve's package)" \
"Compatible: $compatible" "Compatible: $compatible"
if ! ask_yn "Do you understand the risks and want to continue?" n; then if ! ask_yn "Do you understand the risks and want to continue?" n; then
info "BIOS update cancelled; nothing was changed."; rm -rf "$tmp"; return 0 info "BIOS update cancelled; nothing was changed."; rm -rf "$BIOS_STAGE_DIR"; return 0
fi fi
bios_disclaimer "LAST CHANCE: THIS FLASHES BIOS $BIOS_NEWEST" \ bios_disclaimer "LAST CHANCE: THIS FLASHES BIOS $BIOS_NEWEST" \
"After this, keep the power on until the machine has fully" \ "After this, keep the power on until the machine has fully" \
@@ -176,28 +224,10 @@ bios_enable() {
local reply local reply
read -rp "$(echo -e "${c_red}${c_bold}Type UPDATE (in capitals) to flash the BIOS, anything else cancels:${c_reset} ")" reply read -rp "$(echo -e "${c_red}${c_bold}Type UPDATE (in capitals) to flash the BIOS, anything else cancels:${c_reset} ")" reply
if [[ "$reply" != UPDATE ]]; then if [[ "$reply" != UPDATE ]]; then
info "BIOS update cancelled; nothing was changed."; rm -rf "$tmp"; return 0 info "BIOS update cancelled; nothing was changed."; rm -rf "$BIOS_STAGE_DIR"; return 0
fi fi
bios_flash
if [[ -n "$BIOS_DRY_RUN" ]]; then
ok "Dry run: would run: fwupdmgr install -y --no-reboot-check $(basename "$BIOS_CAB")"
ok "Dry run finished; nothing was flashed."
# Treated as staged, so the restart choices that follow a real
# update show up too; restarting only prints (see restart_now).
BIOS_NEEDS_RESTART=1
rm -rf "$tmp"
return 0
fi
info "Handing BIOS $BIOS_NEWEST to fwupd. Do NOT turn off the power from now on."
# -y: we already asked twice; --no-reboot-check: the wizard's own
# restart question comes at the end.
if ! sudo fwupdmgr install -y --no-reboot-check "$tmp/$BIOS_CAB"; then
err "fwupd could not install the BIOS update (see above); the BIOS was not changed."
rm -rf "$tmp"
return 1
fi
rm -rf "$tmp"
BIOS_NEEDS_RESTART=1
ok "BIOS $BIOS_NEWEST is staged. It is written during the next restart:"
warn "keep the power on and don't touch the machine until it has fully started again."
} }
# For the app (lib/backend.sh): the flash step alone, after its warnings.
bios_flash_only() { bios_flash; }
+1 -13
View File
@@ -23,19 +23,7 @@ boot_enable() {
# the session into the base config; run it right away rather than rely # the session into the base config; run it right away rather than rely
# on its path unit, which could still be pending when the login manager # on its path unit, which could still be pending when the login manager
# starts. # starts.
sudo tee "$BOOT_UNIT" > /dev/null << EOF service_file "$BOOT_UNIT_NAME" SESSION_SYNC="$SESSION_SYNC" | sudo tee "$BOOT_UNIT" > /dev/null
[Unit]
Description=Steamify CachyOS: boot into the desktop instead of gaming mode
Before=display-manager.service sddm.service plasmalogin.service
[Service]
Type=oneshot
ExecStart=/usr/lib/steamos/steam-set-session plasma.desktop
ExecStart=/bin/sh -c '[ -x $SESSION_SYNC ] && exec $SESSION_SYNC || true'
[Install]
WantedBy=graphical.target
EOF
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable "$BOOT_UNIT_NAME" || { err "Enabling $BOOT_UNIT_NAME failed."; return 1; } sudo systemctl enable "$BOOT_UNIT_NAME" || { err "Enabling $BOOT_UNIT_NAME failed."; return 1; }
ok "Boots into the desktop from the next boot on (Return to Gaming Mode still works)." ok "Boots into the desktop from the next boot on (Return to Gaming Mode still works)."
+112 -19
View File
@@ -3,14 +3,33 @@
# volume control (cec-audio-control) and the units that attach USB CEC # volume control (cec-audio-control) and the units that attach USB CEC
# adapters (Pulse-Eight, RainShadow) with inputattach. They're only in # adapters (Pulse-Eight, RainShadow) with inputattach. They're only in
# Valve's SteamOS (holo) repository, not in CachyOS or the AUR. Works with # Valve's SteamOS (holo) repository, not in CachyOS or the AUR. Works with
# any /dev/cec*: a GPU with CEC (e.g. the Steam Machine) or a USB adapter. # any /dev/cec*: a GPU with CEC or a USB adapter. On a Steam Machine it also
# On a Steam Machine, steamos-manager configures cecd from Steam's settings. # builds Valve's CEC driver (see cec_driver_enable), and steamos-manager
# configures cecd from Steam's settings.
# Sourced by steamify.sh; not meant to be run on its own. # Sourced by steamify.sh; not meant to be run on its own.
CEC_PKGS=(cecd cec-audio-control inputattach-cec-units) CEC_PKGS=(cecd cec-audio-control inputattach-cec-units)
# steamos-manager checks once, at its start, whether cecd runs; order it # steamos-manager checks once, at its start, whether cecd runs; order it
# after cecd so it never misses it at login. # after cecd so it never misses it at login.
CEC_ORDER_DROPIN=/etc/systemd/user/steamos-manager.service.d/10-steamify-after-cecd.conf CEC_ORDER_DROPIN=/etc/systemd/user/steamos-manager.service.d/10-steamify-after-cecd.conf
# CachyOS's gaming mode script exports STEAM_ENABLE_CEC=0, which hides
# Steam's HDMI-CEC settings; Steam reads it from the gamescope environment
# file. A later EnvironmentFile= overrides it.
CEC_STEAM_ENV=/etc/steamify/steam-cec.env
CEC_STEAM_DROPIN=/etc/systemd/user/steam-launcher.service.d/10-steamify-cec.conf
# The Steam Machine's CEC runs through its embedded controller (cros_ec_cec).
# Mainline's driver doesn't list Fremont, so there's no /dev/cec0; Valve's
# kernel does. We build Valve's copy with DKMS for every kernel, pinned to a
# reviewed commit.
CEC_DKMS_NAME=steamify-cros-ec-cec
CEC_DKMS_VER=1
CEC_DKMS_SRC="/usr/src/$CEC_DKMS_NAME-$CEC_DKMS_VER"
CEC_DRIVER_URL="https://raw.githubusercontent.com/evlaV/linux-integration/10c8c8800ccd3ae359203b4eefb6479f613b3b8e/drivers/media/cec/platform/cros-ec/cros-ec-cec.c"
CEC_DRIVER_SHA256=e89fd4e87fceb32d713ffc59b64794779b3c2b3c012b0e2f2e9f0c40b42f4373
# Downloaded once, then kept: raw.githubusercontent.com rate-limits (HTTP 429) repeated downloads
# from one address, and turning CEC off and on again needs no network then. Named after its
# checksum, so a new pin (a new commit and checksum above) downloads its file once by itself.
CEC_DRIVER_CACHE="/var/cache/steamify/cros-ec-cec-${CEC_DRIVER_SHA256:0:12}.c"
cec_link_steamos_manager() { cec_link_steamos_manager() {
# On a Steam Machine: steamos-manager writes cecd's config from Steam's # On a Steam Machine: steamos-manager writes cecd's config from Steam's
@@ -19,15 +38,78 @@ cec_link_steamos_manager() {
# called by Steam Machine support, which installs steamos-manager after # called by Steam Machine support, which installs steamos-manager after
# this item ran. # this item ran.
pacman -Q steamos-manager >/dev/null 2>&1 || return 0 pacman -Q steamos-manager >/dev/null 2>&1 || return 0
systemctl --user daemon-reload user_systemctl daemon-reload
systemctl --user enable steamos-manager-configure-cecd.service 2>/dev/null user_systemctl enable steamos-manager-configure-cecd.service 2>/dev/null
systemctl --user start steamos-manager-configure-cecd.service 2>/dev/null user_systemctl start steamos-manager-configure-cecd.service 2>/dev/null
systemctl --user restart cecd.service 2>/dev/null user_systemctl restart cecd.service 2>/dev/null
systemctl --user restart steamos-manager.service 2>/dev/null user_systemctl restart steamos-manager.service 2>/dev/null
return 0 return 0
} }
cec_status() { pacman -Q "${CEC_PKGS[@]}" >/dev/null 2>&1; } cec_installed() { pacman -Q "${CEC_PKGS[@]}" >/dev/null 2>&1; }
cec_driver_ok() { ! detect_valve_fremont || [[ -d "$CEC_DKMS_SRC" ]]; }
# On = installed, Steam told to show its CEC settings and, on a Steam
# Machine, the driver built. An install from before 1.1.3 (settings) or
# 2.0.3 (driver) lacks one; the menu then ticks it (see cec_repair).
cec_status() { cec_installed && [[ -f "$CEC_STEAM_DROPIN" ]] && cec_driver_ok; }
cec_reload_driver() {
# cecd keeps /dev/cec0 open, so the old module can't be unloaded under it.
user_systemctl stop cecd.service 2>/dev/null
sudo modprobe -r cros_ec_cec 2>/dev/null
sudo modprobe cros_ec_cec 2>/dev/null
user_systemctl start cecd.service 2>/dev/null
}
cec_driver_enable() {
detect_valve_fremont || return 0
local tmp k
if ! pacman -Q dkms patch >/dev/null 2>&1; then
sudo pacman -S --needed --noconfirm dkms patch || { err "Installing dkms failed."; return 1; }
fi
install_kernel_headers || return 1
tmp="$(mktemp -d)"
if [[ -f "$CEC_DRIVER_CACHE" ]] && echo "$CEC_DRIVER_SHA256 $CEC_DRIVER_CACHE" | sha256sum -c --quiet - 2>/dev/null; then
cp "$CEC_DRIVER_CACHE" "$tmp/cros-ec-cec.c"
else
info "Downloading Valve's Steam Machine CEC driver..."
if ! curl -fsL --retry 2 "$CEC_DRIVER_URL" -o "$tmp/cros-ec-cec.c" ||
! echo "$CEC_DRIVER_SHA256 $tmp/cros-ec-cec.c" | sha256sum -c --quiet -; then
rm -rf "$tmp"
err "Downloading Valve's CEC driver failed (or its checksum didn't match)."
return 1
fi
# The name carries the checksum: a newer pin gets its own file, the old one goes.
sudo find "${CEC_DRIVER_CACHE%/*}" -maxdepth 1 -name 'cros-ec-cec-*.c' ! -name "${CEC_DRIVER_CACHE##*/}" -delete 2>/dev/null
sudo install -Dm644 "$tmp/cros-ec-cec.c" "$CEC_DRIVER_CACHE"
fi
# So it finds amdgpu's HDMI port (see the patch for why).
patch_file cros-ec-cec-single-port.patch | patch -s -d "$tmp" -p1 ||
{ rm -rf "$tmp"; err "Patching Valve's CEC driver failed."; return 1; }
echo 'obj-m += cros-ec-cec.o' >"$tmp/Makefile"
patch_file "$CEC_DKMS_NAME.dkms.conf" | fill NAME="$CEC_DKMS_NAME" VERSION="$CEC_DKMS_VER" >"$tmp/dkms.conf"
sudo dkms remove "$CEC_DKMS_NAME/$CEC_DKMS_VER" --all >/dev/null 2>&1
sudo rm -rf "$CEC_DKMS_SRC"
sudo install -d "$CEC_DKMS_SRC" && sudo install -m644 "$tmp"/{cros-ec-cec.c,Makefile,dkms.conf} "$CEC_DKMS_SRC/"
rm -rf "$tmp"
sudo dkms add "$CEC_DKMS_NAME/$CEC_DKMS_VER" >/dev/null || { err "Adding the CEC driver to DKMS failed."; return 1; }
# Every kernel with headers; one that fails to build only lacks CEC.
for k in /usr/lib/modules/*/build; do
k="$(basename "$(dirname "$k")")"
info "Building the CEC driver for $k..."
sudo dkms install "$CEC_DKMS_NAME/$CEC_DKMS_VER" -k "$k" >/dev/null ||
warn "Building the CEC driver for $k failed; that kernel has no HDMI-CEC."
done
cec_reload_driver
}
cec_driver_disable() {
[[ -d "$CEC_DKMS_SRC" ]] || return 0
sudo dkms remove "$CEC_DKMS_NAME/$CEC_DKMS_VER" --all >/dev/null 2>&1
sudo rm -rf "$CEC_DKMS_SRC"
cec_reload_driver
}
cec_repair() { cec_installed && ! cec_status; }
fetch_holo_pkg() { fetch_holo_pkg() {
# fetch_holo_pkg <dir> <package>: download the newest <package> from # fetch_holo_pkg <dir> <package>: download the newest <package> from
@@ -70,6 +152,7 @@ cec_enable() {
return 1 return 1
fi fi
rm -rf "$tmp" rm -rf "$tmp"
cec_driver_enable || return 1
# Its udev rule gives the user /dev/cec*; cecd starts with the graphical # Its udev rule gives the user /dev/cec*; cecd starts with the graphical
# session, after steamos-manager wrote its config from Steam's settings. # session, after steamos-manager wrote its config from Steam's settings.
sudo udevadm control --reload sudo udevadm control --reload
@@ -77,11 +160,20 @@ cec_enable() {
sudo mkdir -p "$(dirname "$CEC_ORDER_DROPIN")" sudo mkdir -p "$(dirname "$CEC_ORDER_DROPIN")"
printf '%s\n' "# Written by Steamify: steamos-manager only sees cecd if it runs at its start." \ printf '%s\n' "# Written by Steamify: steamos-manager only sees cecd if it runs at its start." \
'[Unit]' 'After=cecd.service' | sudo tee "$CEC_ORDER_DROPIN" >/dev/null '[Unit]' 'After=cecd.service' | sudo tee "$CEC_ORDER_DROPIN" >/dev/null
systemctl --user daemon-reload sudo mkdir -p "$(dirname "$CEC_STEAM_ENV")" "$(dirname "$CEC_STEAM_DROPIN")"
printf '%s\n' "# Written by Steamify: Steam's HDMI-CEC settings in gaming mode." 'STEAM_ENABLE_CEC=1' |
sudo tee "$CEC_STEAM_ENV" >/dev/null
printf '%s\n' "# Written by Steamify: overrides STEAM_ENABLE_CEC=0 from the gaming mode script." \
'[Service]' "EnvironmentFile=-$CEC_STEAM_ENV" | sudo tee "$CEC_STEAM_DROPIN" >/dev/null
user_systemctl daemon-reload
# The TV remote's volume keys: SteamOS enables this socket through its
# preset, which pacman doesn't apply, so nothing else does on CachyOS.
user_systemctl enable --now cec-audio-control.socket 2>/dev/null
cec_link_steamos_manager cec_link_steamos_manager
if compgen -G "/dev/cec*" >/dev/null; then if compgen -G "/dev/cec*" >/dev/null; then
systemctl --user restart cecd.service 2>/dev/null user_systemctl restart cecd.service 2>/dev/null
ok "HDMI-CEC on ($(cd /dev && echo cec*)). Turn on CEC on your TV too (e.g. Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink)." ok "HDMI-CEC on ($(cd /dev && echo cec*)); Steam shows its settings from the next gaming mode start."
info "Turn on CEC on your TV too (e.g. Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink)."
else else
warn "No CEC device (/dev/cec*) found: your GPU may not support CEC. A USB CEC adapter" warn "No CEC device (/dev/cec*) found: your GPU may not support CEC. A USB CEC adapter"
warn "(e.g. Pulse-Eight) works too. Check again after a restart with: ls /dev/cec*" warn "(e.g. Pulse-Eight) works too. Check again after a restart with: ls /dev/cec*"
@@ -90,19 +182,20 @@ cec_enable() {
cec_disable() { cec_disable() {
info "Removing HDMI-CEC..." info "Removing HDMI-CEC..."
systemctl --user disable --now cecd.service cec-audio-control.socket cec-audio-control.service 2>/dev/null user_systemctl disable --now cecd.service cec-audio-control.socket cec-audio-control.service 2>/dev/null
systemctl --user disable steamos-manager-configure-cecd.service 2>/dev/null user_systemctl disable steamos-manager-configure-cecd.service 2>/dev/null
sudo pacman -Rns --noconfirm "${CEC_PKGS[@]}" 2>/dev/null sudo pacman -Rns --noconfirm "${CEC_PKGS[@]}" 2>/dev/null
sudo rm -f "$CEC_ORDER_DROPIN" cec_driver_disable
sudo rmdir "$(dirname "$CEC_ORDER_DROPIN")" 2>/dev/null sudo rm -f "$CEC_ORDER_DROPIN" "$CEC_STEAM_DROPIN" "$CEC_STEAM_ENV"
systemctl --user daemon-reload sudo rmdir "$(dirname "$CEC_ORDER_DROPIN")" "$(dirname "$CEC_STEAM_DROPIN")" "$(dirname "$CEC_STEAM_ENV")" 2>/dev/null
user_systemctl daemon-reload
if [[ -n "$(state_get cec installed_linuxconsole)" ]]; then if [[ -n "$(state_get cec installed_linuxconsole)" ]]; then
sudo pacman -Rns --noconfirm linuxconsole 2>/dev/null sudo pacman -Rns --noconfirm linuxconsole 2>/dev/null
state_clear cec state_clear cec
fi fi
sudo udevadm control --reload sudo udevadm control --reload
systemctl --user is-active -q steamos-manager.service 2>/dev/null && user_systemctl is-active -q steamos-manager.service 2>/dev/null &&
systemctl --user restart steamos-manager.service user_systemctl restart steamos-manager.service
ok "HDMI-CEC removed." ok "HDMI-CEC removed."
} }
@@ -110,5 +203,5 @@ cec_overview() {
# For the menu's kernel overview. # For the menu's kernel overview.
local dev="none" local dev="none"
compgen -G "/dev/cec*" >/dev/null && dev="$(cd /dev && echo cec*)" compgen -G "/dev/cec*" >/dev/null && dev="$(cd /dev && echo cec*)"
echo "CEC devices: $dev cecd: $(systemctl --user is-active cecd 2>/dev/null)" echo "CEC devices: $dev cecd: $(user_systemctl is-active cecd 2>/dev/null)"
} }
+42
View File
@@ -6,6 +6,24 @@ c_reset="\033[0m"; c_bold="\033[1m"; c_green="\033[32m"; c_yellow="\033[33m"; c_
info() { echo -e "${c_cyan}[INFO]${c_reset} $*"; } info() { echo -e "${c_cyan}[INFO]${c_reset} $*"; }
ok() { echo -e "${c_green}[OK]${c_reset} $*"; } ok() { echo -e "${c_green}[OK]${c_reset} $*"; }
# patch_file <name>: a file from patches/ (module sources, patches). The
# single-file build replaces this with one that has them embedded.
patch_file() { cat "$SCRIPT_DIR/patches/$1"; }
# service_file <name> [KEY=value...]: a systemd unit from services/, filled
# in. The single-file build replaces service_raw like patch_file.
service_raw() { cat "$SCRIPT_DIR/services/$1"; }
service_file() { local name="$1"; shift; service_raw "$name" | fill "$@"; }
# fill [KEY=value...]: stdin with each @KEY@ replaced by its value, taken
# literally (a home folder may contain sed's "&", "\" or "|").
fill() {
local kv v
local -a subst=(-e '')
for kv in "$@"; do
v="${kv#*=}"; v="${v//\\/\\\\}"; v="${v//&/\\&}"; v="${v//|/\\|}"
subst+=(-e "s|@${kv%%=*}@|$v|g")
done
sed "${subst[@]}"
}
warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; } warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; }
err() { echo -e "${c_red}[ERROR] $*${c_reset}" >&2; } err() { echo -e "${c_red}[ERROR] $*${c_reset}" >&2; }
ask_yn() { ask_yn() {
@@ -26,6 +44,30 @@ require_root_helper() {
fi fi
} }
user_session() {
# A running user systemd and session bus. Not in the installer's chroot,
# where the ISO applies Steamify for a user who has never logged in.
[[ -S "${XDG_RUNTIME_DIR:-/nonexistent}/bus" ]]
}
user_systemctl() {
# systemctl --user, also without a session: then only the unit files
# change (enable/disable through --root); starting and stopping is left
# to the first login, which starts what's enabled anyway.
if user_session; then systemctl --user "$@"; return; fi
local a verb="" args=()
for a in "$@"; do
[[ "$a" == --now ]] && continue
[[ -z "$verb" && "$a" != -* ]] && verb="$a"
args+=("$a")
done
case "$verb" in
enable|disable|is-enabled|mask|unmask) systemctl --user --root=/ "${args[@]}" ;;
is-active) return 1 ;;
*) return 0 ;;
esac
}
plasma_applets() { plasma_applets() {
# Print "containment:applet" for every applet of plugin $1 in the # Print "containment:applet" for every applet of plugin $1 in the
# user's Plasma layout, for kwriteconfig6 --group paths. # user's Plasma layout, for kwriteconfig6 --group paths.
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# Extended controller support: the Xbox wireless dongle (xone, with its firmware) and Xbox controllers over Bluetooth (xpadneo).
# The kernel has no driver for the dongle, and without xpadneo a Bluetooth Xbox controller works as a plain gamepad (buttons and
# sticks) but misses rumble (trigger motors too), proper button mapping and the battery level. Both are DKMS modules from CachyOS's
# own repo, built for every installed kernel (so every kernel needs its headers). Sourced by steamify.sh; not meant to be run on
# its own.
controller_xone_pkg() {
# A PC with the AUR's xone-dkms-git already works: the repo's xone-dkms would conflict with it.
pacman -Q xone-dkms-git >/dev/null 2>&1 && echo xone-dkms-git || echo xone-dkms
}
controller_pkgs() { echo "xpadneo-dkms $(controller_xone_pkg) xone-dongle-firmware"; }
extended_controller_support_status() {
# shellcheck disable=SC2046 # a list of package names
pacman -Q $(controller_pkgs) >/dev/null 2>&1
}
extended_controller_support_enable() {
local p prev missing=()
info "Installing extended controller support (Xbox wireless dongle, Xbox Bluetooth)..."
install_kernel_headers || return 1
for p in $(controller_pkgs); do pacman -Q "$p" >/dev/null 2>&1 || missing+=("$p"); done
if ((${#missing[@]})); then
sudo pacman -S --needed --noconfirm "${missing[@]}" ||
{ err "Installing ${missing[*]} failed."; return 1; }
# What a first, half-finished run installed stays on the list.
prev="$(state_get extended_controller_support installed_pkgs)"
state_set extended_controller_support installed_pkgs "$(echo "$prev ${missing[*]}" | xargs)"
fi
ok "Extended controller support on: plug the dongle in again (or reconnect the controller) to use it."
}
extended_controller_support_disable() {
local pkgs
pkgs="$(state_get extended_controller_support installed_pkgs)"
# Only what Steamify installed; kernel headers stay.
# shellcheck disable=SC2086 # a list of package names
[[ -n "$pkgs" ]] && sudo pacman -Rns --noconfirm $pkgs >/dev/null 2>&1
state_clear extended_controller_support
ok "Extended controller support off."
}
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
# The ISO applies Steamify in the installer, before the user ever logged
# in (steamify.sh --defaults without a session). What needs Plasma's first
# layout is finished at the first desktop login, by a one-time autostart
# that then opens the app, next to CachyOS Hello.
# Sourced by steamify.sh; not meant to be run on its own.
FIRST_LOGIN_ENTRY="${XDG_CONFIG_HOME:-$HOME/.config}/autostart/steamify-first-login.desktop"
FIRST_LOGIN_SCRIPT="$STEAMIFY_BIN/first-login"
FIRST_LOGIN_BUNDLE="$STEAMIFY_BIN/first-login-steamify.sh"
first_login_schedule() {
# The newest release's Steamify runs at the first login. STEAMIFY_NO_DOWNLOAD (the ISO was started with
# steamify.nodownload, to test an unreleased Steamify): this very bundle runs instead.
# (Only a single-file bundle can be copied; SCRIPT_DIR doesn't exist in one.)
local run self="${BASH_SOURCE[0]:-}"
if [[ -n "${STEAMIFY_NO_DOWNLOAD:-}" && -f "$self" ]] && grep -q '^# Single-file build' "$self"; then
install_executable "$FIRST_LOGIN_BUNDLE" 755 < "$self"
run="bash \"$FIRST_LOGIN_BUNDLE\" --first-login; rm -f \"$FIRST_LOGIN_BUNDLE\""
else
run="curl -fsSL --max-time 30 \"$WIZARD_URL\" | bash -s -- --first-login"
fi
install_executable "$FIRST_LOGIN_SCRIPT" 755 << EOF2
#!/bin/bash
# Installed by Steamify CachyOS: runs once, at the first desktop login.
set -o pipefail
mkdir -p ~/.cache
{ $run; } > ~/.cache/steamify-first-login.log 2>&1
rm -f "$FIRST_LOGIN_ENTRY" "\$0"
[[ -x "$WIZARD_APP_LAUNCHER" ]] && exec "$WIZARD_APP_LAUNCHER"
EOF2
printf '%s\n' '[Desktop Entry]' 'Type=Application' 'Name=Steamify first login' \
"Exec=$FIRST_LOGIN_SCRIPT" 'OnlyShowIn=KDE;' 'X-KDE-autostart-phase=2' 'NoDisplay=true' |
install_executable "$FIRST_LOGIN_ENTRY" 644
}
first_login_run() {
# plasmashell has just laid out the desktop from the look-and-feel
# package; wait for its config, then put single user's launcher
# settings on the new launcher.
local _
for _ in $(seq 30); do
[[ -n "$(plasma_applets org.kde.plasma.kickoff)" ]] && break
sleep 2
done
stop_plasmashell_for_edit
taskbar_drop_discover
single_status && single_launcher
restart_plasmashell_if_stopped
return 0
}
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# "Power-off fix" menu item (poweroff), a sub-option of Steam Machine support
# ticked along with it (opt-out): the Steam Machine powers off instead of
# booting up again. Recent kernels (7.2, and the 6.x, 7.0 and 7.1 updates
# that got the change backported) keep the S4/S5 wake bit the firmware
# leaves set on GPIO pin 18 ("pinctrl-amd: Don't clear S4 wake bits at
# probe"), so the machine starts again right after powering off. Valve's kernel clears it at probe on Fremont, in
# a patch not meant for upstream, so CachyOS won't get it. A small module
# (patches/steamify-fremont-poweroff.c) clears it right before power-off;
# DKMS builds it for each installed kernel. On a kernel that clears it
# itself it does nothing.
# Sourced by steamify.sh; not meant to be run on its own.
POWEROFF_DKMS_NAME=steamify-fremont-poweroff
POWEROFF_DKMS_VER=1
POWEROFF_DKMS_SRC="/usr/src/$POWEROFF_DKMS_NAME-$POWEROFF_DKMS_VER"
POWEROFF_MODULES_LOAD=/etc/modules-load.d/steamify-fremont-poweroff.conf
poweroff_fix_installed() { [[ -f "$POWEROFF_DKMS_SRC/dkms.conf" && -f "$POWEROFF_MODULES_LOAD" ]]; }
poweroff_fix_enable() {
detect_valve_fremont || return 0
local tmp k
if ! pacman -Q dkms >/dev/null 2>&1; then
sudo pacman -S --needed --noconfirm dkms || { err "Installing dkms failed."; return 1; }
fi
install_kernel_headers || return 1
tmp="$(mktemp -d)"
patch_file "$POWEROFF_DKMS_NAME.c" > "$tmp/$POWEROFF_DKMS_NAME.c"
echo "obj-m += $POWEROFF_DKMS_NAME.o" >"$tmp/Makefile"
patch_file "$POWEROFF_DKMS_NAME.dkms.conf" | fill NAME="$POWEROFF_DKMS_NAME" VERSION="$POWEROFF_DKMS_VER" >"$tmp/dkms.conf"
sudo dkms remove "$POWEROFF_DKMS_NAME/$POWEROFF_DKMS_VER" --all >/dev/null 2>&1
sudo rm -rf "$POWEROFF_DKMS_SRC"
sudo install -d "$POWEROFF_DKMS_SRC" &&
sudo install -m644 "$tmp"/{"$POWEROFF_DKMS_NAME.c",Makefile,dkms.conf} "$POWEROFF_DKMS_SRC/"
rm -rf "$tmp"
sudo dkms add "$POWEROFF_DKMS_NAME/$POWEROFF_DKMS_VER" >/dev/null ||
{ err "Adding the power-off fix to DKMS failed."; return 1; }
for k in /usr/lib/modules/*/build; do
k="$(basename "$(dirname "$k")")"
info "Building the power-off fix for $k..."
sudo dkms install "$POWEROFF_DKMS_NAME/$POWEROFF_DKMS_VER" -k "$k" >/dev/null ||
warn "Building the power-off fix for $k failed; with that kernel it may start again after shutting down."
done
echo "$POWEROFF_DKMS_NAME" | sudo tee "$POWEROFF_MODULES_LOAD" >/dev/null
sudo modprobe "$POWEROFF_DKMS_NAME" 2>/dev/null
return 0
}
poweroff_fix_disable() {
sudo rm -f "$POWEROFF_MODULES_LOAD"
sudo modprobe -r "$POWEROFF_DKMS_NAME" 2>/dev/null
[[ -d "$POWEROFF_DKMS_SRC" ]] || return 0
sudo dkms remove "$POWEROFF_DKMS_NAME/$POWEROFF_DKMS_VER" --all >/dev/null 2>&1
sudo rm -rf "$POWEROFF_DKMS_SRC"
}
poweroff_status() { poweroff_fix_installed; }
poweroff_enable() {
info "Installing the power-off fix (the machine stays off after shutting down)..."
poweroff_fix_enable || { err "Installing the power-off fix failed."; return 1; }
ok "Power-off fix on: the Steam Machine stays off after shutting down."
}
poweroff_disable() {
poweroff_fix_disable
ok "Power-off fix removed; with recent kernels the Steam Machine may start again after shutting down."
}
+103
View File
@@ -0,0 +1,103 @@
#!/bin/bash
# "HDMI refresh boost" menu item (hdmi), retired in 2.2.0: it saved an EDID
# override per display (with a hotplug script loading it) so the pinned
# 7.1.6 kernel ran HDMI displays above 60 Hz. Newer kernels read the whole
# EDID themselves, and the kernel pin is gone with the power-off fix, so
# only its removal is left: shown while anything of it is on the system,
# always unticked, so a normal run removes it.
# Sourced by steamify.sh; not meant to be run on its own.
HDMI_FW_DIR=/usr/lib/firmware/edid
# Before 2.1.0 the override was on the kernel command line.
HDMI_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-edid.conf
# The saved displays; their EDIDs are $HDMI_FW_DIR/steamify-<id>.bin.
HDMI_MAP=/etc/steamify/hdmi-edid.conf
HDMI_RUN=/run/steamify-edid
HDMI_HOTPLUG=/usr/local/bin/steamify-edid-hotplug
HDMI_UNIT_NAME=steamify-edid.service
HDMI_UNIT="/etc/systemd/system/$HDMI_UNIT_NAME"
HDMI_UDEV_RULE=/etc/udev/rules.d/90-steamify-edid.rules
hdmi_connectors() {
# Connected HDMI outputs, as "<card>-<connector>" (e.g. card1-HDMI-A-1).
local d
for d in /sys/class/drm/card*-HDMI-A-*; do
[[ "$(cat "$d/status" 2>/dev/null)" == connected ]] && basename "$d"
done
}
hdmi_boot_file() {
# The file holding the kernel command line, per boot loader.
if [[ -f /etc/default/limine ]]; then echo /etc/default/limine
elif [[ -f /etc/sdboot-manage.conf ]] && command -v sdboot-manage >/dev/null; then echo /etc/sdboot-manage.conf
elif [[ -f /etc/default/grub ]]; then echo /etc/default/grub
fi
}
hdmi_cmdline_param() {
# The drm.edid_firmware= value we set, or nothing.
local f; f="$(hdmi_boot_file)"
[[ -n "$f" ]] && grep -oE 'drm\.edid_firmware=[^ "]*steamify-[^ "]*' "$f" 2>/dev/null | head -n 1
}
hdmi_available() { detect_valve_fremont && hdmi_status; }
hdmi_status() {
# On while anything of it is left: saved displays, the hotplug script,
# an EDID file, or the kernel parameter of versions before 2.1.0.
[[ -s "$HDMI_MAP" || -f "$HDMI_UNIT" || -n "$(hdmi_cmdline_param)" ]] ||
compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null
}
hdmi_reset_live() {
# Puts a connected display back on its own EDID right away. debugfs is
# root-only, so the glob runs under sudo; the kernel takes exactly
# "reset", without a newline.
local d
d="$(sudo sh -c 'for d in /sys/kernel/debug/dri/*/"$1"; do [ -e "$d/edid_override" ] && { echo "$d"; break; }; done' _ "${1#card*-}")"
[[ -n "$d" ]] || return 1
printf reset | sudo tee "$d/edid_override" >/dev/null &&
echo 1 | sudo tee "$d/trigger_hotplug" >/dev/null
}
hdmi_remove_boot_param() {
# Removes the drm.edid_firmware= older versions put on the kernel command
# line, then rebuilds the initramfs and boot entries.
local f
[[ -n "$(hdmi_cmdline_param)" ]] || { sudo rm -f "$HDMI_INITRAMFS_CONF"; return 0; }
f="$(hdmi_boot_file)"
backup_file "$f"
sudo sed -i -E 's/drm\.edid_firmware=[^ "]*steamify-[^ "]* ?//' "$f"
sudo rm -f "$HDMI_INITRAMFS_CONF"
info "Rebuilding the initramfs and boot entries..."
case "$f" in
/etc/default/limine) sudo limine-mkinitcpio ;;
/etc/sdboot-manage.conf) sudo /usr/bin/mkinitcpio -P && sudo sdboot-manage gen ;;
/etc/default/grub) sudo /usr/bin/mkinitcpio -P && sudo grub-mkconfig -o /boot/grub/grub.cfg ;;
esac
}
hdmi_forget_all() {
# Every saved display and the hotplug script that loaded their EDIDs.
local conn
sudo rm -f "$HDMI_FW_DIR"/steamify-*.bin
sudo systemctl disable "$HDMI_UNIT_NAME" >/dev/null 2>&1
sudo rm -f "$HDMI_UDEV_RULE" "$HDMI_UNIT" "$HDMI_HOTPLUG" "$HDMI_MAP"
sudo rm -rf "$HDMI_RUN"
sudo systemctl daemon-reload
sudo udevadm control --reload
for conn in $(hdmi_connectors); do hdmi_reset_live "$conn" 2>/dev/null; done
return 0
}
hdmi_enable() {
err "HDMI refresh boost is no longer offered: current kernels run HDMI displays at their full rate."
return 1
}
hdmi_disable() {
hdmi_forget_all
hdmi_remove_boot_param || return 1
state_clear hdmi
ok "HDMI refresh boost removed; displays use their own EDID again."
}
+93 -49
View File
@@ -18,6 +18,78 @@ gaming_status() {
esac esac
} }
# What Steam shows in its System settings besides the OS name (which stays
# CachyOS's, for legal clarity and because limine-snapper-sync finds the boot
# entries by it): the SteamOS release as the OS version, steam-machine as the
# codename, Steamify with its version as the variant (os-release VERSION_ID,
# VERSION_CODENAME, VARIANT_ID; see patches/steamify-os-release.sh).
OS_NAME_SCRIPT=/usr/local/libexec/steamify-os-release
OS_NAME_HOOK=/etc/pacman.d/hooks/zz-steamify-os-release.hook
# limine's tools take the OS name from TARGET_OS_NAME, else os-release's
# PRETTY_NAME: pinned to CachyOS, the boot entries never depend on os-release.
LIMINE_DEFAULTS=/etc/default/limine
steamos_release() {
# The SteamOS release Steamify follows: Valve's newest jupiter-X.Y repo
# (where the SteamOS extras come from), as an os-release ID (no spaces or
# capitals: steamos-3.9). Offline (e.g. in the installer): the one set
# before, if any.
local v
v="$(curl -fsL --max-time 15 "$VALVE_MIRROR/" 2>/dev/null | grep -oE 'jupiter-[0-9]+\.[0-9]+/' | tr -d / | sort -V | tail -n 1)"
if [[ -n "$v" ]]; then echo "steamos-${v#jupiter-}"
else sed -n 's/^VERSION_ID=\(steamos-.*\)$/\1/p' /etc/os-release 2>/dev/null; fi
}
os_name_enable() {
# 2.9.0 pre-releases renamed the OS itself; the name stays CachyOS's.
if grep -q '^DISTRIB_DESCRIPTION=.*with Steamify' /etc/lsb-release 2>/dev/null &&
[[ -x /usr/share/libalpm/scripts/cachyos-branding ]]; then
sudo /usr/share/libalpm/scripts/cachyos-branding lsb-release
fi
[[ -n "$(kreadconfig6 --file kcm-about-distrorc --group General --key Name 2>/dev/null)" ]] &&
kset gaming kcm-about-distrorc General Name --delete
# About this System: os-release's VERSION (CachyOS has none) instead of
# VERSION_ID, which is the SteamOS release now.
kset gaming kcm-about-distrorc General UseOSReleaseVersion true
patch_file steamify-os-release.sh | fill VERSION="$VERSION" STEAMOS="$(steamos_release)" |
sudo install -Dm755 /dev/stdin "$OS_NAME_SCRIPT" &&
patch_file steamify-os-release.hook | fill SCRIPT="$OS_NAME_SCRIPT" |
sudo install -Dm644 /dev/stdin "$OS_NAME_HOOK" &&
sudo "$OS_NAME_SCRIPT" || { err "Setting Steamify's version for Steam failed."; return 1; }
if [[ -f "$LIMINE_DEFAULTS" ]] && ! grep -q '^TARGET_OS_NAME=' "$LIMINE_DEFAULTS"; then
backup_file "$LIMINE_DEFAULTS"
printf '%s\n' 'TARGET_OS_NAME="CachyOS"' | sudo tee -a "$LIMINE_DEFAULTS" > /dev/null
state_set gaming limine_target 1
fi
return 0
}
os_name_disable() {
sudo rm -f "$OS_NAME_HOOK" "$OS_NAME_SCRIPT"
[[ -f /etc/os-release ]] && sudo sed -i -e '/^VARIANT=/d' -e '/^VARIANT_ID=/d' -e '/^VERSION_ID=/d' -e '/^VERSION_CODENAME=/d' /etc/os-release
# CachyOS's own values back, the way its hooks write them.
if [[ -x /usr/share/libalpm/scripts/cachyos-branding ]]; then
sudo /usr/share/libalpm/scripts/cachyos-branding os-release
# Undoes 2.9.0 pre-releases, which changed lsb-release's name too.
sudo /usr/share/libalpm/scripts/cachyos-branding lsb-release
fi
if [[ -n "$(state_get gaming limine_target)" ]]; then
sudo sed -i '/^TARGET_OS_NAME="CachyOS"$/d' "$LIMINE_DEFAULTS"
state_set gaming limine_target ""
fi
return 0
}
os_version_refresh() {
# After every run that changed something (sudo is at hand then): Steam's
# OS version follows Steamify's, not just the one that set it up. Silent:
# the app reads the backend's stdout as JSON.
gaming_status 2>/dev/null || return 0
grep -qx "VARIANT_ID=steamify-$VERSION" /etc/os-release 2>/dev/null && return 0
os_name_enable > /dev/null 2>&1
return 0
}
gaming_enable() { gaming_enable() {
# LOGIN_MANAGER (sddm with single user, else plasmalogin) is set by the # LOGIN_MANAGER (sddm with single user, else plasmalogin) is set by the
# menu. Re-running with the other value switches over cleanly. # menu. Re-running with the other value switches over cleanly.
@@ -37,6 +109,7 @@ gaming_enable() {
esac esac
create_desktop_shortcut create_desktop_shortcut
steam_enable steam_enable
os_name_enable || return 1
} }
gaming_disable() { gaming_disable() {
@@ -49,6 +122,8 @@ gaming_disable() {
switch_to_plasmalogin switch_to_plasmalogin
remove_desktop_shortcut remove_desktop_shortcut
steam_disable steam_disable
krevert gaming
os_name_disable
ok "The PC boots to the normal login screen again (from the next boot)." ok "The PC boots to the normal login screen again (from the next boot)."
} }
@@ -92,6 +167,20 @@ switch_to_sddm() {
ok "SDDM is the login manager from the next boot on." ok "SDDM is the login manager from the next boot on."
} }
remove_sddm_base_autologin() {
# /etc/sddm.conf is read last: an [Autologin] there would override our fragments.
if [[ -f /etc/sddm.conf ]] && grep -q '^\[Autologin\]' /etc/sddm.conf; then
backup_file /etc/sddm.conf
sudo awk '
/^\[Autologin\]/ { skip=1; next }
/^\[/ { skip=0 }
!skip { print }
' /etc/sddm.conf | sudo tee /etc/sddm.conf.tmp > /dev/null &&
sudo mv /etc/sddm.conf.tmp /etc/sddm.conf
info "Removed [Autologin] from /etc/sddm.conf (backup kept)."
fi
}
configure_sddm_autologin() { configure_sddm_autologin() {
# User= and Relogin= live in our own fragment; steam-set-session keeps # User= and Relogin= live in our own fragment; steam-set-session keeps
# Session= up to date in zz-steamos-autologin.conf, which sorts later # Session= up to date in zz-steamos-autologin.conf, which sorts later
@@ -107,16 +196,7 @@ Session=gamescope-session.desktop
Relogin=true Relogin=true
EOF EOF
if [[ -f /etc/sddm.conf ]] && grep -q '^\[Autologin\]' /etc/sddm.conf; then remove_sddm_base_autologin
backup_file /etc/sddm.conf
sudo awk '
/^\[Autologin\]/ { skip=1; next }
/^\[/ { skip=0 }
!skip { print }
' /etc/sddm.conf | sudo tee /etc/sddm.conf.tmp > /dev/null &&
sudo mv /etc/sddm.conf.tmp /etc/sddm.conf
info "Removed [Autologin] from /etc/sddm.conf (backup kept)."
fi
# Start in gamescope, through CachyOS's own tool so its file is current. # Start in gamescope, through CachyOS's own tool so its file is current.
sudo /usr/lib/steamos/steam-set-session gamescope-session.desktop sudo /usr/lib/steamos/steam-set-session gamescope-session.desktop
@@ -185,24 +265,7 @@ install_session_sync() {
local SYNC_SCRIPT="/usr/local/bin/sync-steamos-session.sh" local SYNC_SCRIPT="/usr/local/bin/sync-steamos-session.sh"
info "Installing session sync bridge at $SYNC_SCRIPT" info "Installing session sync bridge at $SYNC_SCRIPT"
sudo tee "$SYNC_SCRIPT" > /dev/null << 'EOF' patch_file sync-steamos-session.sh | sudo tee "$SYNC_SCRIPT" > /dev/null
#!/bin/bash
SRC="/etc/plasmalogin.conf.d/zz-steamos-autologin.conf"
DEST="/etc/plasmalogin.conf"
[[ -f "$SRC" ]] || exit 0
SESSION=$(grep -oP '^Session=\K.*' "$SRC")
[[ -z "$SESSION" ]] && exit 0
# Only touch Session= inside [Autologin]; other sections may have their own.
if sed -n '/^\[Autologin\]/,/^\[/p' "$DEST" | grep -q '^Session='; then
sed -i "/^\[Autologin\]/,/^\[/ s|^Session=.*|Session=$SESSION|" "$DEST"
elif grep -q '^\[Autologin\]' "$DEST"; then
sed -i "/^\[Autologin\]/a Session=$SESSION" "$DEST"
else
printf '\n[Autologin]\nSession=%s\n' "$SESSION" >> "$DEST"
fi
EOF
sudo chmod +x "$SYNC_SCRIPT" sudo chmod +x "$SYNC_SCRIPT"
ok "Sync script installed." ok "Sync script installed."
@@ -211,28 +274,9 @@ EOF
# 7. systemd path watcher + service # 7. systemd path watcher + service
info "Installing systemd path watcher so session switches take effect immediately" info "Installing systemd path watcher so session switches take effect immediately"
sudo tee /etc/systemd/system/sync-steamos-session.path > /dev/null << 'EOF' service_file sync-steamos-session.path | sudo tee /etc/systemd/system/sync-steamos-session.path > /dev/null
[Unit]
Description=Watch for steamos session changes
[Path] service_file sync-steamos-session.service | sudo tee /etc/systemd/system/sync-steamos-session.service > /dev/null
PathModified=/etc/plasmalogin.conf.d/zz-steamos-autologin.conf
[Install]
WantedBy=multi-user.target
EOF
sudo tee /etc/systemd/system/sync-steamos-session.service > /dev/null << 'EOF'
[Unit]
Description=Sync steamos session selection into plasmalogin.conf
# Session switches can come in bursts (switch + autologin reset); never
# let systemd's start rate limit silently stop the bridge.
StartLimitIntervalSec=0
[Service]
Type=oneshot
ExecStart=/usr/local/bin/sync-steamos-session.sh
EOF
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable --now sync-steamos-session.path sudo systemctl enable --now sync-steamos-session.path
+253 -20
View File
@@ -5,35 +5,70 @@
# Menu order. Components are turned on in this order and off in reverse; # Menu order. Components are turned on in this order and off in reverse;
# gaming must come first (single user builds on it). # gaming must come first (single user builds on it).
COMPONENTS=(gaming boot theme glyphs single launcher cec machine kpin bios) COMPONENTS=(gaming boot silent glyphs nvidia bigpicture nvsilent theme single launcher steamgame notify extended_controller_support vram cec machine poweroff kpin hdmi bios)
# One-off actions rather than on/off components: never preselected, never # One-off actions rather than on/off components: never preselected, never
# re-applied, not listed as on or off. # re-applied, not listed as on or off.
ACTIONS=(bios) ACTIONS=(bios)
# Sub-options, shown indented under their parent and only while it's ticked. # Sub-options, shown indented under their parent and only while it's ticked.
declare -A PARENT=([boot]=gaming [kpin]=machine) declare -A PARENT=([boot]=gaming [silent]=gaming [glyphs]=gaming [bigpicture]=nvidia [nvsilent]=nvidia [steamgame]=launcher [poweroff]=machine [kpin]=machine [hdmi]=machine [bios]=machine)
# Never preselected on a first run: booting into the desktop is a choice, # Never preselected on a first run: booting into the desktop is a choice,
# gamescope is the default; HDMI-CEC is opt-in (it can wake the machine or # gamescope is the default; HDMI-CEC is opt-in (it can wake the machine or
# upset other devices on the TV, even on SteamOS), except on a Steam Machine, # upset other devices on the TV, even on SteamOS), except on a Steam Machine,
# which has CEC like on SteamOS. # which has CEC like on SteamOS. HDMI refresh boost needs someone at the
NO_PRESELECT=(boot cec) # screen to confirm each step. Extended controller support builds kernel modules for
# every installed kernel: opt-in.
NO_PRESELECT=(boot silent nvsilent cec kpin hdmi extended_controller_support)
# Feature versions: the Steamify version in which what a component's enable
# sets up last changed; set it to the new VERSION whenever you change one.
# Each successful run records it (state "features"); a component that's on
# with an older one is ticked and re-applied by a normal run, and the app
# shows it as an update. A setup from before these were recorded counts as
# FEATURE_BASELINE.
FEATURE_BASELINE=2.1.0
declare -A FEATURE_VERSION=(
[gaming]=2.9.0 [boot]=2.1.0 [silent]=2.11.0 [nvidia]=2.10.0 [bigpicture]=2.10.0 [nvsilent]=2.11.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0
[launcher]=2.1.0 [cec]=2.7.0 [machine]=2.9.0 [poweroff]=2.2.0 [vram]=2.3.0 [notify]=2.5.0 [steamgame]=2.5.1 [extended_controller_support]=2.10.0
[kpin]=2.1.0 [hdmi]=2.1.0
)
declare -A LABEL=( declare -A LABEL=(
[gaming]="SteamOS conversion: boot into gaming mode, Steam on the desktop" [gaming]="SteamOS conversion: boot into gaming mode, Steam on the desktop"
[boot]="Boot into the desktop instead of gaming mode" [boot]="Boot into the desktop instead of gaming mode"
[nvidia]="Gaming on NVIDIA: Steam on the desktop, started at login"
[silent]="Start Steam silently in desktop mode: in the tray at login on the desktop, no window"
[nvsilent]="Start Steam silently: in the tray at login, no window (untick: Steam's normal window)"
[bigpicture]="Steam starts in Big Picture: the controller-friendly Steam (untick: Steam's normal window)"
[theme]="Install SteamOS theme: Vapor look (cachyos-vapor)" [theme]="Install SteamOS theme: Vapor look (cachyos-vapor)"
[glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode" [glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode"
[single]="Single user mode: no password, lock screen or log out (SDDM)" [single]="Single user mode: no password, lock screen or log out (SDDM)"
[launcher]="Steamify shortcut: desktop icon to run this again" [launcher]="Steamify shortcut: the app on the desktop, Steamify Terminal in the launcher"
[steamgame]="Add as non-Steam game: Steamify in your Steam library, for the controller and gaming mode"
[notify]="Update notifications: a notification when there's a new Steamify, never updates by itself"
[extended_controller_support]="Extended controller support: Xbox wireless dongle (xone), Xbox controllers over Bluetooth (xpadneo)"
[vram]="VRAM booster: the game in front keeps its VRAM, background apps make room"
[cec]="HDMI-CEC: use Steam with the TV remote, TV on/off with the PC (experimental)" [cec]="HDMI-CEC: use Steam with the TV remote, TV on/off with the PC (experimental)"
[machine]="Steam Machine support: LED bar driver, hardware settings in Steam" [machine]="Steam Machine support: LED bar driver, hardware settings in Steam"
[kpin]="Pin the kernel to $PINNED_KERNEL_VER (fixes rebooting after shutdown)" [poweroff]="Power-off fix: the Steam Machine stays off after shutting down"
[kpin]="Pin the kernel to $PINNED_KERNEL_VER (untick for CachyOS's current kernel)"
[hdmi]="HDMI refresh boost: highest refresh your HDMI display runs"
[bios]="Update BIOS" [bios]="Update BIOS"
) )
declare -A CURRENT WANTED declare -A CURRENT WANTED
component_available() { component_available() {
case "$1" in case "$1" in
machine|kpin) machine_available ;; # gamescope's own session is broken on NVIDIA: the conversion is replaced by "Gaming on NVIDIA" there (shown
# anyway when it's already on, so it can be turned off).
gaming|boot|silent|glyphs) ! nvidia_present || gaming_status ;;
nvidia) nvidia_available ;;
bigpicture|nvsilent) bigpicture_available ;;
machine|poweroff) machine_available ;;
vram) vram_available ;;
# Steamify in the Steam library is for gaming mode's controller: not needed for Big Picture on the desktop (NVIDIA PCs;
# shown anyway when it's already on, so it can be turned off).
steamgame) steamgame_available && { ! nvidia_present || steamgame_status; } ;;
kpin) kpin_available ;;
hdmi) hdmi_available ;;
bios) bios_available ;; bios) bios_available ;;
esac esac
} }
@@ -50,47 +85,241 @@ menu_visible() {
# Shown in the menu. A sub-option ("Boot into", the kernel pin) only # Shown in the menu. A sub-option ("Boot into", the kernel pin) only
# while its parent is ticked. # while its parent is ticked.
component_available "$1" || return 1 component_available "$1" || return 1
# Silent start is hidden while Steam starts in Big Picture.
[[ "$1" == nvsilent && "${WANTED[bigpicture]:-0}" == 1 ]] && return 1
[[ -z "${PARENT[$1]:-}" || "${WANTED[${PARENT[$1]}]:-0}" == 1 ]] [[ -z "${PARENT[$1]:-}" || "${WANTED[${PARENT[$1]}]:-0}" == 1 ]]
} }
feature_record_unticked() {
# After a run the user confirmed: options shown but left unticked that
# have no record yet count as turned off, or the next run would offer a
# default one again as "new" (feature_new) and tick it: e.g. single user
# mode unticked on a first run came back ticked when anything else
# changed. Hidden sub-options weren't a choice, so they're left alone.
local c
for c in "${COMPONENTS[@]}"; do
is_action "$c" && continue
menu_visible "$c" || continue
[[ "${WANTED[$c]:-0}" == 0 && -z "$(state_get features "$c")" ]] && state_set features "$c" off
done
return 0
}
feature_record() {
# feature_record <component> <enable|disable>: after a successful run.
if [[ "$2" == enable ]]; then state_set features "$1" "${FEATURE_VERSION[$1]:-$FEATURE_BASELINE}"
else state_set features "$1" off; fi
}
feature_outdated() {
# On, but set up by an older version of that feature (compared as
# versions: 2.10.0 is newer than 2.9.0).
local have want
[[ "${CURRENT[$1]:-0}" == 1 ]] || return 1
have="$(state_get features "$1" "$FEATURE_BASELINE")"
[[ "$have" == off ]] && have="$FEATURE_BASELINE"
want="${FEATURE_VERSION[$1]:-$FEATURE_BASELINE}"
[[ "$have" != "$want" && "$(printf '%s\n' "$have" "$want" | sort -V | head -n 1)" == "$have" ]]
}
menu_label() {
# The label, with "(update)" after the name when a newer version of it
# will be applied, "(new)" for a default sub-option added since the
# last run.
local l="${LABEL[$1]}" b=""
if feature_outdated "$1"; then b="${c_yellow}(update)${c_reset}"
elif feature_new "$1" || feature_new_optin "$1"; then b="${c_green}(new)${c_reset}"; fi
if [[ -n "$b" ]]; then
if [[ "$l" == *:* ]]; then l="${l%%:*} $b:${l#*:}"
else l+=" $b"; fi
fi
printf '%s' "$l"
}
feature_added() {
# An option added after its parent was set up (e.g. the power-off fix under
# Steam Machine support; a top-level one counts the SteamOS conversion, or
# "Gaming on NVIDIA" where that replaces it, as its parent), not on yet,
# with no record: shown as "new".
local p="${PARENT[$1]:-}" parent_on=0
if [[ -n "$p" ]]; then parent_on="${CURRENT[$p]:-0}"
elif [[ "${CURRENT[gaming]:-0}" == 1 || "${CURRENT[nvidia]:-0}" == 1 ]]; then parent_on=1; fi
[[ "$1" != gaming && "$parent_on" == 1 && "${CURRENT[$1]:-0}" == 0 ]] &&
! is_action "$1" && component_selectable "$1" &&
[[ -z "$(state_get features "$1")" ]]
}
feature_new() {
# A new default option: ticked for you (never turned on or off by itself).
feature_added "$1" && [[ " ${NO_PRESELECT[*]} " != *" $1 "* ]]
}
feature_new_optin() {
# A new opt-in option (unticked by default): the "new" badge only, it stays
# unticked. Boot into is a choice row of the conversion, not an item.
feature_added "$1" && [[ "$1" != boot && " ${NO_PRESELECT[*]} " == *" $1 "* ]]
}
component_selectable() { component_selectable() {
# Greyed out and not tickable when it has nothing to do. # Greyed out and not tickable when it has nothing to do.
case "$1" in case "$1" in
bios) bios_selectable ;; bios) bios_selectable ;;
vram) vram_selectable ;;
esac esac
} }
detect_components() { detect_components() {
local c any=false local c any=false
for c in "${COMPONENTS[@]}"; do CURRENT[$c]=0; WANTED[$c]=0; done
for c in "${COMPONENTS[@]}"; do for c in "${COMPONENTS[@]}"; do
component_available "$c" || continue component_available "$c" || continue
if "${c}_status"; then CURRENT[$c]=1; any=true; else CURRENT[$c]=0; fi if "${c}_status"; then CURRENT[$c]=1; any=true; else CURRENT[$c]=0; fi
WANTED[$c]=${CURRENT[$c]} WANTED[$c]=${CURRENT[$c]}
done done
# HDMI-CEC set up by an older version: tick it, so a normal run fixes it.
component_available cec && cec_repair && WANTED[cec]=1
# Updated features and new default sub-options: ticked, so a normal run
# applies them.
for c in "${COMPONENTS[@]}"; do
component_available "$c" || continue
{ feature_outdated "$c" || feature_new "$c"; } && WANTED[$c]=1
done
# The kernel pin is no longer needed (the power-off fix), and HDMI
# refresh boost needed the pin: whatever is left of either is unticked,
# so a normal run removes it.
component_available kpin && WANTED[kpin]=0
component_available hdmi && WANTED[hdmi]=0
# The terminal-only Steamify shortcut from before 2.0.1: tick it, so a
# normal run replaces it with the app.
launcher_repair && WANTED[launcher]=1
# Shows the current and newest BIOS version. # Shows the current and newest BIOS version.
bios_available && { bios_lookup_newest; LABEL[bios]="$(bios_label)"; } bios_available && { bios_lookup_newest; LABEL[bios]="$(bios_label)"; }
# Greyed out with an NVIDIA card: say why, and what to do.
if component_available vram && ! vram_selectable; then
VRAM_NVIDIA_CASE="$(vram_nvidia_case)"
LABEL[vram]="VRAM booster: $(vram_nvidia_hint "$VRAM_NVIDIA_CASE")"
fi
# First run: preselect the full SteamOS experience (never an action). # First run: preselect the full SteamOS experience (never an action).
if [[ "$any" == false ]]; then if [[ "$any" == false ]]; then
for c in "${COMPONENTS[@]}"; do for c in "${COMPONENTS[@]}"; do
component_available "$c" && ! is_action "$c" && component_available "$c" && component_selectable "$c" && ! is_action "$c" &&
[[ " ${NO_PRESELECT[*]} " != *" $c "* ]] && WANTED[$c]=1 [[ " ${NO_PRESELECT[*]} " != *" $c "* ]] && WANTED[$c]=1
done done
machine_available && WANTED[cec]=1 machine_available && WANTED[cec]=1
fi fi
} }
defaults_options() {
# defaults_options [--options <id>[,<id>...]] [--boot gamescope|desktop]:
# what --defaults sets up, from the Steam Machine ISO's installer page.
# --options: exactly these items on, every other off (without it, the
# menu's preselection). An item that's on brings its parent (single user
# the conversion, like ticking it in the menu); one this PC can't use is
# left off with a warning. --boot desktop needs the conversion. Returns 1
# on an unknown option, item or value.
local c ids list=false boot=""
while [[ $# -gt 0 ]]; do
# Both take a value: without one, shift 2 fails and never shifts, so
# the loop would spin forever.
[[ "$1" == --options || "$1" == --boot ]] && [[ $# -lt 2 ]] && { err "$1 needs a value"; return 1; }
case "$1" in
--options)
list=true
IFS=, read -ra ids <<< "$2"
shift 2 ;;
--boot)
case "$2" in
gamescope|desktop) boot="$2" ;;
*) err "--boot takes gamescope or desktop"; return 1 ;;
esac
shift 2 ;;
*) err "Unknown option: $1"; return 1 ;;
esac
done
if [[ "$list" == true ]]; then
for c in "${ids[@]}"; do
[[ -n "$c" ]] || continue
[[ -n "${LABEL[$c]:-}" ]] || { err "Unknown item: $c"; return 1; }
[[ "$c" == boot ]] && { err "Where to start is --boot, not an item."; return 1; }
is_action "$c" && { err "$c can't be part of the install."; return 1; }
done
for c in "${COMPONENTS[@]}"; do WANTED[$c]=0; done
for c in "${ids[@]}"; do
[[ -n "$c" ]] || continue
if component_available "$c" && component_selectable "$c"; then
WANTED[$c]=1
else
warn "Leaving out ${LABEL[$c]%%:*}: not available on this PC."
fi
done
[[ "${WANTED[single]}" == 1 ]] && component_available gaming && WANTED[gaming]=1
for c in "${COMPONENTS[@]}"; do
[[ "${WANTED[$c]}" == 1 && -n "${PARENT[$c]:-}" ]] && WANTED[${PARENT[$c]}]=1
done
fi
case "$boot" in
desktop)
[[ "${WANTED[gaming]}" == 1 ]] || { err "--boot desktop needs the SteamOS conversion (gaming)."; return 1; }
WANTED[boot]=1 ;;
gamescope) WANTED[boot]=0 ;;
esac
return 0
}
defaults_list() {
# --defaults --list: what --defaults can set up on this PC, as one JSON
# array, for an installer page (the Steam Machine ISO builds its Steamify
# page from it, so new items show up without a new ISO). Per item: id,
# label, hint, kind (toggle, or choice for boot: on = desktop), parent,
# on (preselected like a first run) and selectable. Actions, retired
# items and items this PC can't use (e.g. no Steam account yet) are
# left out. Read-only: no sudo, no state.
local c kind on sel items=""
for c in "${COMPONENTS[@]}"; do
component_available "$c" || continue
is_action "$c" && continue
[[ "$c" == kpin || "$c" == hdmi ]] && continue
kind=toggle; [[ "$c" == boot ]] && kind=choice
sel=false; component_selectable "$c" && sel=true
on=false
[[ "$sel" == true && " ${NO_PRESELECT[*]} " != *" $c "* ]] && on=true
[[ "$c" == cec ]] && machine_available && on=true
items+="${items:+,}{\"id\":$(json_str "$c"),\"label\":$(json_str "${LABEL[$c]%%:*}")"
items+=",\"hint\":$(json_str "$( [[ "${LABEL[$c]}" == *:* ]] && echo "${LABEL[$c]#*: }")")"
items+=",\"kind\":\"$kind\",\"parent\":$(json_str "${PARENT[$c]:-}"),\"needs\":\"\",\"hideWhen\":$(json_str "$([[ "$c" == nvsilent ]] && echo bigpicture)"),\"on\":$on,\"selectable\":$sel}"
done
printf '[%s]\n' "$items"
}
toggle_component() { toggle_component() {
local c="$1" local c="$1"
component_selectable "$c" || return 1 component_selectable "$c" || return 1
WANTED[$c]=$(( 1 - WANTED[$c] )) WANTED[$c]=$(( 1 - WANTED[$c] ))
# Single user mode only makes sense on top of the SteamOS conversion. # Single user mode only makes sense on top of the SteamOS conversion.
if [[ "$c" == single && "${WANTED[single]}" == 1 ]]; then WANTED[gaming]=1; fi # (On NVIDIA the conversion isn't offered: single user mode logs in by itself, see lib/single-user.sh.)
if [[ "$c" == single && "${WANTED[single]}" == 1 ]] && component_available gaming; then WANTED[gaming]=1; fi
if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then WANTED[single]=0; WANTED[boot]=0; fi if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then WANTED[single]=0; WANTED[boot]=0; fi
# Where to boot to is part of the conversion, too. # Where to boot to is part of the conversion, too.
if [[ "$c" == boot && "${WANTED[boot]}" == 1 ]]; then WANTED[gaming]=1; fi if [[ "$c" == boot && "${WANTED[boot]}" == 1 ]]; then WANTED[gaming]=1; fi
# The kernel pin is opt-out: ticked along with Steam Machine support. # Big Picture is opt-out: ticked along with "Gaming on NVIDIA".
if [[ "$c" == machine ]]; then WANTED[kpin]=${WANTED[machine]}; fi if [[ "$c" == nvidia ]]; then WANTED[bigpicture]=${WANTED[nvidia]}; fi
if [[ "$c" == bigpicture && "${WANTED[bigpicture]}" == 1 ]]; then WANTED[nvidia]=1; WANTED[nvsilent]=0; fi
# Silent and Big Picture exclude each other.
if [[ "$c" == nvsilent && "${WANTED[nvsilent]}" == 1 ]]; then WANTED[nvidia]=1; WANTED[bigpicture]=0; fi
if [[ "$c" == nvidia && "${WANTED[nvidia]}" == 0 ]]; then WANTED[nvsilent]=0; fi
if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then WANTED[silent]=0; WANTED[glyphs]=0; fi
if [[ "$c" == silent && "${WANTED[silent]}" == 1 ]]; then WANTED[gaming]=1; fi
if [[ "$c" == glyphs && "${WANTED[glyphs]}" == 1 ]]; then WANTED[gaming]=1; fi
# The power-off fix is opt-out: ticked along with Steam Machine support.
if [[ "$c" == machine ]]; then WANTED[poweroff]=${WANTED[machine]}; fi
if [[ "$c" == poweroff && "${WANTED[poweroff]}" == 1 ]]; then WANTED[machine]=1; fi
if [[ "$c" == machine && "${WANTED[machine]}" == 0 ]]; then WANTED[kpin]=0; WANTED[bios]=0; fi
if [[ "$c" == kpin && "${WANTED[kpin]}" == 1 ]]; then WANTED[machine]=1; fi if [[ "$c" == kpin && "${WANTED[kpin]}" == 1 ]]; then WANTED[machine]=1; fi
if [[ "$c" == bios && "${WANTED[bios]}" == 1 ]]; then WANTED[machine]=1; fi
# Steam starts Steamify through the shortcut's start script.
# Opt-out, like the power-off fix: ticked along with the shortcut.
if [[ "$c" == launcher ]] && component_available steamgame; then WANTED[steamgame]=${WANTED[launcher]}; fi
if [[ "$c" == steamgame && "${WANTED[steamgame]}" == 1 ]]; then WANTED[launcher]=1; fi
} }
show_menu() { show_menu() {
@@ -113,12 +342,13 @@ show_menu() {
[[ "${CURRENT[gaming]}" == 1 ]] || now="-" [[ "${CURRENT[gaming]}" == 1 ]] || now="-"
want="gaming"; [[ "${WANTED[boot]}" == 1 ]] && want="desk" want="gaming"; [[ "${WANTED[boot]}" == 1 ]] && want="desk"
printf " %-3s %-6s %-6s └ %s\n" "$i" "$now" "$want" "$(boot_choice "${WANTED[boot]}")" printf " %-3s %-6s %-6s └ %s\n" "$i" "$now" "$want" "$(boot_choice "${WANTED[boot]}")"
elif ! component_selectable "$c"; then
local tree=""; [[ -n "${PARENT[$c]:-}" ]] && tree=" └ "
printf " %b%-3s %-6s %-6s %s%s (not available)%b\n" "$c_dim" "$i" "$now" "$want" "$tree" "${LABEL[$c]}" "$c_reset"
elif [[ -n "${PARENT[$c]:-}" ]]; then elif [[ -n "${PARENT[$c]:-}" ]]; then
printf " %-3s %b %-6s └ %s\n" "$i" "$now" "$want" "${LABEL[$c]}" printf " %-3s %b %-6s └ %b\n" "$i" "$now" "$want" "$(menu_label "$c")"
elif component_selectable "$c"; then
printf " %-3s %b %-6s %s\n" "$i" "$now" "$want" "${LABEL[$c]}"
else else
printf " %b%-3s %-6s %-6s %s (not available)%b\n" "$c_dim" "$i" "$now" "$want" "${LABEL[$c]}" "$c_reset" printf " %-3s %b %-6s %b\n" "$i" "$now" "$want" "$(menu_label "$c")"
fi fi
done done
echo echo
@@ -166,8 +396,9 @@ draw_menu_tui() {
fi fi
if ! component_selectable "$c"; then if ! component_selectable "$c"; then
# Greyed out: nothing to do (e.g. BIOS already up to date). # Greyed out: nothing to do (e.g. BIOS already up to date).
local mark=" "; (( i == cursor )) && mark=" ${c_cyan}>${c_reset} " local mark=" " tree=""; (( i == cursor )) && mark=" ${c_cyan}>${c_reset} "
echo -e "${mark}${c_dim}[ ] ${LABEL[$c]} (not available)${c_reset}" [[ -n "${PARENT[$c]:-}" ]] && tree=" └ "
echo -e "${mark}${c_dim}${tree}[ ] ${LABEL[$c]} (not available)${c_reset}"
elif (( i == cursor )); then elif (( i == cursor )); then
# The green x's reset would end the bold too: re-enable it after. # The green x's reset would end the bold too: re-enable it after.
echo -e " ${c_cyan}>${c_reset} ${c_bold}${line//"$c_reset"/"$c_reset$c_bold"}${c_reset}${state}" echo -e " ${c_cyan}>${c_reset} ${c_bold}${line//"$c_reset"/"$c_reset$c_bold"}${c_reset}${state}"
@@ -250,7 +481,7 @@ plan_changes() {
component_available "$c" || continue component_available "$c" || continue
[[ "${WANTED[$c]}" == 1 ]] || continue [[ "${WANTED[$c]}" == 1 ]] || continue
if is_action "$c"; then TO_ENABLE+=("$c"); continue; fi if is_action "$c"; then TO_ENABLE+=("$c"); continue; fi
if [[ "${CURRENT[$c]}" == 0 || "$REAPPLY" == true ]] || if [[ "${CURRENT[$c]}" == 0 || "$REAPPLY" == true ]] || feature_outdated "$c" ||
[[ "$c" == gaming && "${CURRENT[single]}" != "${WANTED[single]}" ]]; then [[ "$c" == gaming && "${CURRENT[single]}" != "${WANTED[single]}" ]]; then
TO_ENABLE+=("$c") TO_ENABLE+=("$c")
fi fi
@@ -265,13 +496,15 @@ apply_changes() {
for c in "${TO_DISABLE[@]}"; do for c in "${TO_DISABLE[@]}"; do
if [[ "$c" == boot ]]; then echo; echo -e "${c_bold}Boot into: gamescope${c_reset}" if [[ "$c" == boot ]]; then echo; echo -e "${c_bold}Boot into: gamescope${c_reset}"
else echo; echo -e "${c_bold}Turning off: ${LABEL[$c]}${c_reset}"; fi else echo; echo -e "${c_bold}Turning off: ${LABEL[$c]}${c_reset}"; fi
"${c}_disable" || failed+=("$c") if "${c}_disable"; then feature_record "$c" disable; else failed+=("$c"); fi
done done
for c in "${TO_ENABLE[@]}"; do for c in "${TO_ENABLE[@]}"; do
if [[ "$c" == boot ]]; then echo; echo -e "${c_bold}Boot into: desktop${c_reset}" if [[ "$c" == boot ]]; then echo; echo -e "${c_bold}Boot into: desktop${c_reset}"
elif is_action "$c"; then echo; echo -e "${c_bold}Running: ${LABEL[$c]%%:*}${c_reset}" elif is_action "$c"; then echo; echo -e "${c_bold}Running: ${LABEL[$c]%%:*}${c_reset}"
else echo; echo -e "${c_bold}Turning on: ${LABEL[$c]}${c_reset}"; fi else echo; echo -e "${c_bold}Turning on: ${LABEL[$c]}${c_reset}"; fi
"${c}_enable" || failed+=("$c") if "${c}_enable"; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi
done done
feature_record_unticked
os_version_refresh
FAILED=("${failed[@]}") FAILED=("${failed[@]}")
} }
+267
View File
@@ -0,0 +1,267 @@
#!/bin/bash
# Gaming on NVIDIA PCs: the SteamOS conversion (gamescope session) isn't offered there, because gamescope's DRM session shows a
# corrupted picture on NVIDIA (NVIDIA's open bug 5240452: modes above 2560x1440@120). `nvidia` is the replacement: Steam on the
# Plasma desktop, installed when missing and started at login; `bigpicture` (a sub-option) makes it start in Big Picture.
# On a supported card (RTX 20 series or newer, `nvidia_supported`) it also sets up what NVIDIA's Wayland/DRM stack wants:
# nvidia-drm.modeset=1 and fbdev=1 on the kernel command line and the NVIDIA modules in the initramfs. That didn't cure
# gamescope's picture (the driver already reported both `Y`), but it is the standard setup and may be needed.
# Sourced by steamify.sh; not meant to be run on its own.
NVIDIA_UNIT=steamify-steam-autostart.service
NVIDIA_PARAMS="nvidia-drm.modeset=1 nvidia-drm.fbdev=1"
NVIDIA_INITRAMFS_CONF=${NVIDIA_INITRAMFS_CONF:-/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf}
# The script and pacman hook that keep the initramfs file right at every kernel change.
NVIDIA_SCRIPT=${NVIDIA_SCRIPT:-/usr/local/libexec/steamify-nvidia-initramfs}
NVIDIA_HOOK=${NVIDIA_HOOK:-/etc/pacman.d/hooks/85-steamify-nvidia-initramfs.hook}
# The running kernel's command line; the tests point this at a file.
NVIDIA_CMDLINE=${NVIDIA_CMDLINE:-/proc/cmdline}
# The DRM devices in sysfs; the tests point this at a fake tree.
NVIDIA_DRM_DIR=${NVIDIA_DRM_DIR:-/sys/class/drm}
nvidia_unit_file() { echo "$HOME/.config/systemd/user/$NVIDIA_UNIT"; }
nvidia_present() {
# An NVIDIA GPU (PCI vendor 0x10de, display class) whose driver provides
# nvidia_drm. nouveau has none, and gamescope is not a problem there.
local d
for d in "$NVIDIA_DRM_DIR"/card[0-9]*/device; do
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de ]] || continue
[[ "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue
modinfo nvidia_drm >/dev/null 2>&1 && return 0
done
return 1
}
# Shown on NVIDIA PCs, unless the SteamOS conversion is already on there (it
# has to be turned off first: both would start Steam at login).
nvidia_available() { nvidia_present && ! gaming_status 2>/dev/null; }
bigpicture_available() { nvidia_available; }
nvidia_status() {
pacman -Q steam >/dev/null 2>&1 && [[ -f "$(nvidia_unit_file)" ]] &&
user_systemctl is-enabled -q "$NVIDIA_UNIT" 2>/dev/null
}
nvidia_write_unit() {
# nvidia_write_unit [args]: the autostart unit, Steam started with those arguments.
local unit; unit="$(nvidia_unit_file)"
mkdir -p "$(dirname "$unit")"
service_file steamify-steam-autostart.service ARGS="$1" > "$unit" || { err "Writing $unit failed."; return 1; }
user_systemctl daemon-reload
}
nvidia_enable() {
info "Gaming on NVIDIA: Steam on the desktop, started at login..."
if ! pacman -Q steam >/dev/null 2>&1; then
sudo pacman -S --needed --noconfirm steam || { err "Installing Steam failed."; return 1; }
state_set nvidia installed_pkgs steam
fi
# Keeps what the Big Picture option set up; a first run starts Steam normally.
local unit_args=""
if bigpicture_status; then unit_args=-gamepadui; elif nvsilent_status; then unit_args=-silent; fi
nvidia_write_unit "$unit_args" || return 1
user_systemctl enable "$NVIDIA_UNIT" >/dev/null 2>&1 || { err "Enabling $NVIDIA_UNIT failed."; return 1; }
nvidia_kernel_enable || return 1
ok "Steam starts at your next login."
}
nvidia_disable() {
local pkgs unit; unit="$(nvidia_unit_file)"
user_systemctl disable --now "$NVIDIA_UNIT" >/dev/null 2>&1
rm -f "$unit"
user_systemctl daemon-reload
pkgs="$(state_get nvidia installed_pkgs)"
# Only what Steamify installed; games and the Steam folder stay.
# shellcheck disable=SC2086 # a list of package names
[[ -n "$pkgs" ]] && sudo pacman -Rns --noconfirm $pkgs >/dev/null 2>&1
state_clear nvidia
krevert bigpicture
nvidia_kernel_disable
ok "Steam no longer starts at login."
}
bigpicture_status() { grep -qs -- '-gamepadui' "$(nvidia_unit_file)"; }
bigpicture_enable() {
# The unit comes from nvidia_enable (it runs first); without it there is nothing to change.
[[ -f "$(nvidia_unit_file)" ]] || return 0
nvidia_write_unit -gamepadui || return 1
# Windows of the last session (Discord, a browser) would be restored on top of Big Picture.
kset bigpicture ksmserverrc General loginMode emptySession
ok "Steam starts in Big Picture at your next login, on an empty desktop."
}
bigpicture_disable() {
krevert bigpicture
[[ -f "$(nvidia_unit_file)" ]] || return 0
nvidia_write_unit "" && ok "Steam starts in its normal window at your next login."
}
# Sub-option: Steam starts silently (tray only, no window). Big Picture and
# a silent start exclude each other (the menus untick one when the other is
# ticked); a first run with both gets Big Picture.
nvsilent_status() { grep -qs -- '-silent' "$(nvidia_unit_file)"; }
nvsilent_enable() {
[[ -f "$(nvidia_unit_file)" ]] || return 0
bigpicture_status && return 0
nvidia_write_unit -silent || return 1
ok "Steam starts silently at your next login."
}
nvsilent_disable() {
nvsilent_status || return 0
nvidia_write_unit "" && ok "Steam starts in its normal window at your next login."
}
# --- Kernel parameters and early modules (a supported card only)
nvidia_supported() {
# A supported NVIDIA GPU: PCI vendor 0x10de, display class, its kernel
# driver installed, and not older than the RTX 20 series, decided like the
# VRAM booster does (vram_nvidia_legacy_id: chwd's legacy card lists, every
# other card runs NVIDIA's open modules). Older cards are left alone.
local d
for d in "$NVIDIA_DRM_DIR"/card[0-9]*/device; do
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de ]] || continue
[[ "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue
vram_nvidia_legacy_id "$(cat "$d/device" 2>/dev/null)" && continue
modinfo nvidia_drm >/dev/null 2>&1 && return 0
done
return 1
}
nvidia_legacy_gpu() {
# 0 when the only NVIDIA GPU(s) are older than RTX 20 (reported, not fixed).
local d found=0
for d in "$NVIDIA_DRM_DIR"/card[0-9]*/device; do
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de && "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue
vram_nvidia_legacy_id "$(cat "$d/device" 2>/dev/null)" || return 1
found=1
done
[[ $found == 1 ]]
}
nvidia_boot_file() {
# Same boot loaders as the kernel command line elsewhere (lib/hdmi-refresh.sh).
hdmi_boot_file
}
nvidia_param_set() {
# 0 when $1 (e.g. nvidia-drm.modeset=1) is on the kernel command line or
# already in the boot loader's file (not booted yet).
# The kernel treats "nvidia_drm" and "nvidia-drm" alike: CachyOS' own
# setup may already have written the underscore spelling.
local f re; f="$(nvidia_boot_file)"; re="${1//nvidia-drm/nvidia[-_]drm}"
grep -qwE -- "$re" "$NVIDIA_CMDLINE" 2>/dev/null ||
{ [[ -n "$f" ]] && grep -qwE -- "$re" "$f" 2>/dev/null; }
}
nvidia_params_missing() {
local p
for p in $NVIDIA_PARAMS; do nvidia_param_set "$p" || return 0; done
return 1
}
nvidia_initramfs_ok() {
[[ -f "$NVIDIA_INITRAMFS_CONF" ]] || grep -qE '^MODULES=.*nvidia_drm' /etc/mkinitcpio.conf 2>/dev/null
}
nvidia_modules_everywhere() {
# 0 when every installed kernel has the nvidia_drm module (the script's own check).
patch_file steamify-nvidia-initramfs.sh | NVIDIA_MODULES_DIR="${NVIDIA_MODULES_DIR:-/usr/lib/modules}" bash -s check
}
nvidia_hook_install() {
# Never installs an empty script or hook (a piped patch_file that failed
# still lets install succeed with no input).
local script hook
script="$(patch_file steamify-nvidia-initramfs.sh)" && [[ -n "$script" ]] &&
hook="$(patch_file steamify-nvidia-initramfs.hook | fill SCRIPT="$NVIDIA_SCRIPT")" && [[ -n "$hook" ]] || return 1
printf '%s\n' "$script" | sudo install -Dm755 /dev/stdin "$NVIDIA_SCRIPT" &&
printf '%s\n' "$hook" | sudo install -Dm644 /dev/stdin "$NVIDIA_HOOK"
}
nvidia_rebuild_boot() {
local f; f="$(nvidia_boot_file)"
info "Rebuilding the initramfs and boot entries..."
case "$f" in
/etc/default/limine) sudo limine-mkinitcpio ;;
/etc/sdboot-manage.conf) sudo /usr/bin/mkinitcpio -P && sudo sdboot-manage gen ;;
/etc/default/grub) sudo /usr/bin/mkinitcpio -P && sudo grub-mkconfig -o /boot/grub/grub.cfg ;;
*) sudo /usr/bin/mkinitcpio -P ;;
esac
}
nvidia_boot_has_params() {
# 0 when the generated boot entries carry the parameters (or can't be
# checked): limine.conf, systemd-boot's entries, grub.cfg.
local target
case "$1" in
*/limine) target="${NVIDIA_LIMINE_CONF:-/boot/limine.conf}" ;;
*/sdboot-manage.conf) target="${NVIDIA_SDBOOT_DIR:-/boot/loader/entries}" ;;
*/grub) target="${NVIDIA_GRUB_CFG:-/boot/grub/grub.cfg}" ;;
*) return 0 ;;
esac
sudo test -e "$target" || return 0
sudo grep -rqs 'nvidia-drm.modeset=1' "$target"
}
nvidia_kernel_enable() {
nvidia_supported || return 0
local f changed=0 p; f="$(nvidia_boot_file)"
if nvidia_params_missing; then
if [[ -z "$f" ]]; then
warn "NVIDIA GPU found, but no supported boot loader: add '$NVIDIA_PARAMS' to the kernel command line yourself."
else
info "NVIDIA GPU found: adding $NVIDIA_PARAMS to the kernel command line (fixes a corrupted screen in gaming mode)..."
backup_file "$f"
case "$f" in
*/limine)
sudo sed -i -E "s/^(KERNEL_CMDLINE\[default\]\+?=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;;
*/sdboot-manage.conf)
sudo sed -i -E "s/^(LINUX_OPTIONS=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;;
*/grub)
sudo sed -i -E "s/^(GRUB_CMDLINE_LINUX_DEFAULT=\"[^\"]*)\"/\1 $NVIDIA_PARAMS\"/" "$f" ;;
esac
if ! grep -q "nvidia-drm.fbdev=1" "$f"; then
err "Couldn't add the NVIDIA parameters to $f (no kernel command line setting in it): add '$NVIDIA_PARAMS' yourself."
return 1
fi
changed=1
fi
fi
nvidia_hook_install || { err "Installing the NVIDIA initramfs hook failed."; return 1; }
local had=0; [[ -f "$NVIDIA_INITRAMFS_CONF" ]] && had=1
sudo env NVIDIA_INITRAMFS_CONF="$NVIDIA_INITRAMFS_CONF" NVIDIA_MODULES_DIR="${NVIDIA_MODULES_DIR:-/usr/lib/modules}" "$NVIDIA_SCRIPT" sync
if [[ -f "$NVIDIA_INITRAMFS_CONF" && $had == 0 ]]; then
info "Loading the NVIDIA modules early (initramfs)..."
changed=1
elif [[ ! -f "$NVIDIA_INITRAMFS_CONF" && $had == 1 ]]; then
warn "A kernel without the NVIDIA modules is installed: not loading them early any more (rebuilding the initramfs without them)."
changed=1
elif [[ ! -f "$NVIDIA_INITRAMFS_CONF" ]] && ! nvidia_initramfs_ok; then
warn "Not every installed kernel has the NVIDIA modules: not loading them early for now (the kernel parameters still apply; this is checked again at every kernel update)."
fi
if [[ $changed == 1 ]]; then
nvidia_rebuild_boot || { err "Rebuilding the boot entries failed."; return 1; }
if ! nvidia_boot_has_params "$f"; then
err "The boot entries don't have the NVIDIA parameters: the initramfs or boot entry build failed (see above)."
return 1
fi
ok "NVIDIA gaming mode fix applied; it takes effect after a reboot."
fi
return 0
}
nvidia_kernel_disable() {
local f changed=0; f="$(nvidia_boot_file)"
if [[ -n "$f" ]] && grep -q "$NVIDIA_PARAMS" "$f" 2>/dev/null; then
sudo sed -i "s/ \?$NVIDIA_PARAMS//" "$f"
changed=1
fi
[[ -f "$NVIDIA_INITRAMFS_CONF" ]] && { sudo rm -f "$NVIDIA_INITRAMFS_CONF"; changed=1; }
sudo rm -f "$NVIDIA_HOOK" "$NVIDIA_SCRIPT"
[[ $changed == 1 ]] && nvidia_rebuild_boot
return 0
}
+57
View File
@@ -81,6 +81,13 @@ single_wallet_disable() {
ok "Your own wallet is back (the empty one is kept as kdewallet.kwl$WALLET_OURS)." ok "Your own wallet is back (the empty one is kept as kdewallet.kwl$WALLET_OURS)."
} }
reload_screen_locker() {
# The screen locker only reads kscreenlockerrc when the session starts:
# without this it keeps locking after idling until the next login.
qdbus6 org.freedesktop.ScreenSaver /ScreenSaver org.kde.screensaver.configure >/dev/null 2>&1 ||
busctl --user call org.freedesktop.ScreenSaver /ScreenSaver org.kde.screensaver configure >/dev/null 2>&1 || true
}
single_launcher() { single_launcher() {
# Launcher: only Sleep / Restart / Shut Down, no Session dropdown (where # Launcher: only Sleep / Restart / Shut Down, no Session dropdown (where
# Log Out lives). Restricting action/logout instead would also hide # Log Out lives). Restricting action/logout instead would also hide
@@ -94,7 +101,54 @@ single_launcher() {
done done
} }
single_launcher_drop() {
# The theme's layout backup is taken when it's turned on: with single
# user on then, it holds single's launcher keys, and restoring it after
# single was turned off brings them back. Drop exactly single's values
# (a launcher set up differently by the user is left alone).
local applet
local -a kick_args
for applet in $(plasma_applets org.kde.plasma.kickoff); do
kick_args=(--file plasma-org.kde.plasma.desktop-appletsrc --group Containments --group "${applet%%:*}"
--group Applets --group "${applet#*:}" --group Configuration --group General)
[[ "$(kreadconfig6 "${kick_args[@]}" --key primaryActions)" == 3 ]] &&
kwriteconfig6 "${kick_args[@]}" --key primaryActions --delete
[[ "$(kreadconfig6 "${kick_args[@]}" --key systemFavorites)" == 'suspend,reboot,shutdown' ]] &&
kwriteconfig6 "${kick_args[@]}" --key systemFavorites --delete
done
return 0
}
# Without the SteamOS conversion (NVIDIA PCs: gamescope's session is broken there) nothing logs in by itself, so single user
# mode does it: SDDM, autologin into the Plasma session. Named to sort after steam-set-session's zz-steamos-autologin.conf,
# which may still say gamescope from an earlier conversion.
SINGLE_AUTOLOGIN=${SINGLE_AUTOLOGIN:-/etc/sddm.conf.d/zzz-steamify-autologin.conf}
single_login_needed() { nvidia_present && ! gaming_status; }
single_login_enable() {
single_login_needed || return 0
switch_to_sddm || return 1
info "Logging in to the desktop by itself (SDDM autologin for $TARGET_USER)..."
sudo mkdir -p "$(dirname "$SINGLE_AUTOLOGIN")"
sudo tee "$SINGLE_AUTOLOGIN" > /dev/null << EOF
[Autologin]
User=$TARGET_USER
Session=plasma.desktop
Relogin=true
EOF
remove_sddm_base_autologin
}
single_login_disable() {
[[ -f "$SINGLE_AUTOLOGIN" ]] || return 0
sudo rm -f "$SINGLE_AUTOLOGIN"
# The way back is CachyOS's own login manager, unless the conversion is on (it keeps SDDM).
gaming_status || switch_to_plasmalogin
}
single_enable() { single_enable() {
single_login_enable || return 1
info "Turning off the lock screen, user switching and logging out..." info "Turning off the lock screen, user switching and logging out..."
stop_plasmashell_for_edit stop_plasmashell_for_edit
@@ -114,11 +168,13 @@ single_enable() {
single_launcher single_launcher
restart_plasmashell_if_stopped restart_plasmashell_if_stopped
reload_screen_locker
single_wallet_enable single_wallet_enable
ok "Single user: no lock screen, user switching or log out." ok "Single user: no lock screen, user switching or log out."
} }
single_disable() { single_disable() {
single_login_disable
info "Restoring the lock screen, user switching and logging out..." info "Restoring the lock screen, user switching and logging out..."
stop_plasmashell_for_edit stop_plasmashell_for_edit
if has_journal single; then if has_journal single; then
@@ -143,6 +199,7 @@ single_disable() {
done done
fi fi
restart_plasmashell_if_stopped restart_plasmashell_if_stopped
reload_screen_locker
single_wallet_disable single_wallet_disable
ok "KDE's normal lock screen and user switching are back." ok "KDE's normal lock screen and user switching are back."
} }
+59 -1
View File
@@ -5,7 +5,11 @@
# deletes the key if it didn't exist before). # deletes the key if it didn't exist before).
# Sourced by steamify.sh; not meant to be run on its own. # Sourced by steamify.sh; not meant to be run on its own.
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/cachyos-gamescope-boot" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/steamify"
# Steamify's own files in the home folder: the app (app/, from
# steamify-app.sh) and the shortcut's and notifier's scripts (bin/).
STEAMIFY_DATA="${XDG_DATA_HOME:-$HOME/.local/share}/steamify"
STEAMIFY_BIN="$STEAMIFY_DATA/bin"
UNSET=$'\x01' UNSET=$'\x01'
kset() { kset() {
@@ -77,3 +81,57 @@ state_clear() {
current_display_manager() { current_display_manager() {
systemctl show -p Id --value display-manager 2>/dev/null | sed 's/\.service$//' systemctl show -p Id --value display-manager 2>/dev/null | sed 's/\.service$//'
} }
migrate_dir() {
# migrate_dir <old> <new>: moves <old> to <new> and leaves a symlink.
# When both exist (an older release ran after a newer one), what <new>
# lacks is moved over; a file in both keeps <new>'s copy and <old> stays
# as it is. Returns 0 when something moved.
local old="$1" new="$2" f moved=1
[[ -d "$old" && ! -L "$old" ]] || return 1
if [[ ! -e "$new" ]]; then
mkdir -p "$(dirname "$new")"
mv "$old" "$new" && ln -s "$new" "$old"
return 0
fi
for f in "$old"/* "$old"/.[!.]*; do
[[ -e "$f" || -L "$f" ]] || continue
[[ -e "$new/${f##*/}" ]] && continue
mv "$f" "$new/" && moved=0
done
rmdir "$old" 2>/dev/null && ln -s "$new" "$old"
return "$moved"
}
migrate_layout() {
# Before 2.5.0 the state and scripts were under the project's old name
# (cachyos-gamescope-boot). Moved once; the old names stay as symlinks,
# so an older release that still runs finds the same state.
local old_state="${XDG_STATE_HOME:-$HOME/.local/state}/cachyos-gamescope-boot"
local old_bin="${XDG_DATA_HOME:-$HOME/.local/share}/cachyos-gamescope-boot"
local old_icon="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps/cachyos-gamescope-boot-wizard.svg"
local f
# Links left pointing at nothing once their component was turned off.
for f in "$old_state" "$old_bin"; do [[ -L "$f" && ! -e "$f" ]] && rm -f "$f"; done
migrate_dir "$old_state" "$STATE_DIR"
if migrate_dir "$old_bin" "$STEAMIFY_BIN"; then
# The notifier had the old paths built in; its unit and the
# shortcut's entries point at the scripts.
[[ -f "$STEAMIFY_BIN/steamify-notifier" ]] &&
patch_file steamify-notifier.py | install_executable "$STEAMIFY_BIN/steamify-notifier" 755
for f in "$NOTIFY_UNITS/$NOTIFY_NAME.service" "$WIZARD_APP_ENTRY" "$WIZARD_TERMINAL_ENTRY" \
"$(wizard_desktop_dir)/$WIZARD_DESKTOP_NAME"; do
[[ -f "$f" ]] && sed -i "s|$old_bin/|$STEAMIFY_BIN/|g" "$f"
done
user_systemctl daemon-reload 2>/dev/null
fi
if [[ -f "$old_icon" ]]; then
if [[ -e "$WIZARD_ICON" ]]; then rm -f "$old_icon"; else mv "$old_icon" "$WIZARD_ICON"; fi
gtk-update-icon-cache -q -f -t "$(dirname "$(dirname "$(dirname "$WIZARD_ICON")")")" 2>/dev/null || true
fi
for f in "$WIZARD_APP_ENTRY" "$WIZARD_TERMINAL_ENTRY" "$(wizard_desktop_dir)/$WIZARD_DESKTOP_NAME"; do
[[ -f "$f" ]] && grep -q '^Icon=cachyos-gamescope-boot-wizard$' "$f" &&
sed -i 's|^Icon=cachyos-gamescope-boot-wizard$|Icon=steamify|' "$f"
done
return 0
}
+43 -31
View File
@@ -5,51 +5,63 @@
steam_enable() { steam_enable() {
info "Applying non-optional SteamOS environment & keyboard fixes..." info "Applying non-optional SteamOS environment & keyboard fixes..."
local steam_env_dir systemd_user_dir local steam_env_dir
steam_env_dir="$HOME/.config/environment.d" steam_env_dir="$HOME/.config/environment.d"
mkdir -p "$steam_env_dir" mkdir -p "$steam_env_dir"
# 2. Prevent KDE Wayland from blocking the virtual keyboard overlay # 2. Prevent KDE Wayland from blocking the virtual keyboard overlay
echo "KWIN_IM_SHOW_ALWAYS=1" > "$steam_env_dir/99-kde-virtual-keyboard.conf" echo "KWIN_IM_SHOW_ALWAYS=1" > "$steam_env_dir/99-kde-virtual-keyboard.conf"
# 3. Setup robust background systemd service for Steam autostart on Desktop Mode ok "Steam UI parameters and keyboard fix deployed successfully."
systemd_user_dir="$HOME/.config/systemd/user" }
mkdir -p "$systemd_user_dir"
cat << 'EOF' > "$systemd_user_dir/steam-desktop-autostart.service" # Sub-option of the SteamOS conversion: Steam starts silently (in the tray,
[Unit] # no window) on the Plasma desktop. It is Steam's own autostart entry, the file
Description=Steam Background Autostart for Virtual Keyboard # Steam's setting "Run Steam when my computer starts" creates and removes, so
After=graphical-session.target # that setting shows what Steamify did (and the other way around). Steam's copy
PartOf=graphical-session.target # is the system steam.desktop; ours has -silent in its Exec line.
STEAM_AUTOSTART="${XDG_CONFIG_HOME:-$HOME/.config}/autostart/steam.desktop"
STEAM_SYSTEM_DESKTOP=/usr/share/applications/steam.desktop
[Service] silent_status() {
Type=simple # The old systemd unit counts as on too (until it is migrated by an update of this option).
# Only in the Plasma desktop session; gamescope-session starts its own Steam. grep -qs '^Exec=.*-silent' "$STEAM_AUTOSTART" ||
ExecCondition=/bin/sh -c '[ "$XDG_CURRENT_DESKTOP" = KDE ]' [[ -f "$HOME/.config/systemd/user/steam-desktop-autostart.service" ]]
ExecStart=/usr/bin/steam -silent }
Restart=on-failure
RestartSec=5
[Install] silent_old_unit_remove() {
WantedBy=graphical-session.target # Before 2.11.0 the option was a systemd user unit: it would start Steam a second time.
EOF local unit="$HOME/.config/systemd/user/steam-desktop-autostart.service"
[[ -f "$unit" ]] || return 0
user_systemctl disable --now steam-desktop-autostart.service 2>/dev/null
rm -f "$unit"
user_systemctl daemon-reload
}
# Clean out old .desktop shortcut so they don't fight silent_enable() {
rm -f "$HOME/.config/autostart/steam.desktop" silent_old_unit_remove
mkdir -p "$(dirname "$STEAM_AUTOSTART")"
if [[ -f "$STEAM_SYSTEM_DESKTOP" ]]; then
sed 's|^Exec=/usr/bin/steam %U$|Exec=/usr/bin/steam -silent %U|' "$STEAM_SYSTEM_DESKTOP" > "$STEAM_AUTOSTART"
fi
silent_status || printf '%s\n' '[Desktop Entry]' 'Name=Steam' 'Exec=/usr/bin/steam -silent %U' \
'Icon=steam' 'Terminal=false' 'Type=Application' 'Categories=Network;FileTransfer;Game;' > "$STEAM_AUTOSTART"
ok "Steam starts silently on the desktop from the next login."
}
# Reload user systemd context and activate the background loop silent_disable() {
systemctl --user daemon-reload silent_old_unit_remove
systemctl --user enable --now steam-desktop-autostart.service silent_status && rm -f "$STEAM_AUTOSTART"
ok "Steam no longer starts by itself on the desktop."
ok "Steam UI parameters and keyboard autostart deployed successfully."
} }
steam_disable() { steam_disable() {
info "Removing Steam desktop autostart..." info "Removing Steam desktop settings..."
systemctl --user disable --now steam-desktop-autostart.service 2>/dev/null # The autostart entry is the `silent` option's: removed with it, and here
rm -f "$HOME/.config/systemd/user/steam-desktop-autostart.service" \ # too for when the conversion goes off by itself.
"$HOME/.config/environment.d/99-kde-virtual-keyboard.conf" silent_status && silent_disable
systemctl --user daemon-reload silent_old_unit_remove
rm -f "$HOME/.config/environment.d/99-kde-virtual-keyboard.conf"
ok "Steam desktop settings removed (takes full effect at next login)." ok "Steam desktop settings removed (takes full effect at next login)."
} }
+129
View File
@@ -0,0 +1,129 @@
#!/bin/bash
# "Add as non-Steam game" (steamgame), a sub-option of the Steamify shortcut:
# Steamify CachyOS in the Steam library, so Steam can start it: with Steam
# Input it then gets the controller (a Steam Controller only talks to Steam
# while Steam runs), in gaming mode too. Steam reads its non-Steam games
# (userdata/<id>/config/shortcuts.vdf, binary; patches/steam-shortcuts.py)
# only at startup and writes them back on exit, so Steam is closed for the
# edit and started again. That's refused while a Steam session depends on it:
# in gaming mode, or with this run started from Steam itself. What it did is
# recorded per Steam account (state steamgame, "<account>": "<appid> added"
# or "<appid> updated", for an entry that was there already): Steam rewrites
# the file on exit, so a mark inside the entry could be lost; the start
# script's path in it is the other sign it's ours.
# Sourced by steamify.sh; not meant to be run on its own.
STEAM_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/Steam"
STEAMGAME_NAME="Steamify CachyOS"
# Steam shows no SVG icons: the shortcut's icon rendered as PNG.
STEAMGAME_ICON="$STEAMIFY_DATA/steamify.png"
steamgame_files() {
# Every Steam account's shortcuts.vdf (the file may not exist yet).
local d
for d in "$STEAM_ROOT"/userdata/*/config; do
[[ -d "$d" && "$(basename "$(dirname "$d")")" != 0 ]] && echo "$d/shortcuts.vdf"
done
}
steamgame_tool() {
python3 <(patch_file steam-shortcuts.py) "$@"
}
steamgame_available() { [[ -n "$(steamgame_files)" ]]; }
steamgame_status() {
local f
while IFS= read -r f; do
[[ -f "$f" ]] && steamgame_tool find "$f" "$WIZARD_APP_LAUNCHER" >/dev/null && return 0
done < <(steamgame_files)
return 1
}
steamgame_steam_busy() {
# Closing Steam would end what's running: gaming mode is Steam's own
# session, and a run started from Steam is one of its games.
if [[ -n "${SteamGameId:-}" ]]; then
err "Steamify was started from Steam, which has to close for this. Start it from the desktop icon, or run Steamify Terminal."
return 0
fi
if pgrep -x gamescope >/dev/null || pgrep -f gamescope-session >/dev/null; then
err "Steam has to close for this, which would end gaming mode. Do it from the desktop."
return 0
fi
return 1
}
steamgame_edit() {
# steamgame_edit <function>: runs it with Steam closed, then starts Steam
# again if it was running (silently, in the tray).
local was_running=false i
if pgrep -x steam >/dev/null; then
steamgame_steam_busy && return 1
was_running=true
info "Closing Steam for a moment (it only reads its games list at startup)..."
# A Steam that's still starting ignores the request: ask again.
for ((i = 0; i < 90; i++)); do
((i % 15 == 0)) && steam -shutdown >/dev/null 2>&1
pgrep -x steam >/dev/null || break
sleep 1
done
pgrep -x steam >/dev/null && { err "Steam didn't close; nothing changed."; return 1; }
fi
"$1"; local rc=$?
if [[ "$was_running" == true ]]; then
systemd-run --user --collect -q steam -silent >/dev/null 2>&1
info "Steam is starting again."
fi
return "$rc"
}
steamgame_icon() {
# The PNG, or the SVG when it can't be rendered (librsvg comes with KDE).
if [[ -f "$WIZARD_ICON" ]] && command -v rsvg-convert >/dev/null &&
mkdir -p "$STEAMIFY_DATA" && rsvg-convert -w 256 -h 256 -o "$STEAMGAME_ICON" "$WIZARD_ICON" 2>/dev/null; then
echo "$STEAMGAME_ICON"
else
echo "$WIZARD_ICON"
fi
}
steamgame_add_all() {
local f out icon
icon="$(steamgame_icon)"
while IFS= read -r f; do
# A copy of each account's list before the first change.
[[ -f "$f" && ! -f "$STATE_DIR/shortcuts-$(basename "$(dirname "$(dirname "$f")")").vdf" ]] &&
mkdir -p "$STATE_DIR" && cp -a "$f" "$STATE_DIR/shortcuts-$(basename "$(dirname "$(dirname "$f")")").vdf"
out="$(steamgame_tool add "$f" "$STEAMGAME_NAME" "$WIZARD_APP_LAUNCHER" "$icon" "$WIZARD_APP_ENTRY")" ||
{ err "Couldn't add Steamify to $f."; return 1; }
state_set steamgame "$(basename "$(dirname "$(dirname "$f")")")" "${out#* } ${out%% *}"
info "Steam library ($(basename "$(dirname "$(dirname "$f")")")): ${out%% *} $STEAMGAME_NAME."
done < <(steamgame_files)
}
steamgame_remove_all() {
local f id
while IFS= read -r f; do
[[ -f "$f" ]] || continue
# Any entry for our start script: it can't start anything without it.
while id="$(steamgame_tool find "$f" "$WIZARD_APP_LAUNCHER")"; do
steamgame_tool remove "$f" "$id" || return 1
done
done < <(steamgame_files)
}
steamgame_enable() {
[[ -x "$WIZARD_APP_LAUNCHER" ]] || { err "The Steamify shortcut isn't set up; turn that on first."; return 1; }
info "Adding $STEAMGAME_NAME to your Steam library..."
steamgame_edit steamgame_add_all || return 1
ok "Steamify is in your Steam library (non-Steam games): start it from there for the controller, in gaming mode too."
}
steamgame_disable() {
info "Removing $STEAMGAME_NAME from your Steam library..."
steamgame_edit steamgame_remove_all || return 1
state_clear steamgame
rm -f "$STEAMGAME_ICON"
ok "Steamify removed from your Steam library."
}
+105 -46
View File
@@ -1,6 +1,7 @@
#!/bin/bash #!/bin/bash
# "Steam Machine support" menu item, only on Valve Fremont hardware: the # "Steam Machine support" menu item, only on Valve Fremont hardware: the
# front LED bar driver, LED access for Steam, and steamos-manager. # front LED bar driver, LED access for Steam and steamos-manager. Its
# sub-option "Power-off fix" is lib/fremont-poweroff.sh.
# Sourced by steamify.sh; not meant to be run on its own. # Sourced by steamify.sh; not meant to be run on its own.
detect_valve_fremont() { detect_valve_fremont() {
@@ -14,8 +15,8 @@ detect_valve_fremont() {
[[ "$vendor" == "OEM" && "$product" == "F7F" ]] [[ "$vendor" == "OEM" && "$product" == "F7F" ]]
} }
# Kernel pinned on a Steam Machine: with newer linux-cachyos releases it # Kernel pinned on a Steam Machine (optional): newer linux-cachyos releases
# reboots instead of shutting down. The packages are kept in # rebooted it instead of shutting down, which the power-off fix now handles. The packages are kept in
# PINNED_KERNEL_DIR, so re-applying (or reinstalling after an update slipped # PINNED_KERNEL_DIR, so re-applying (or reinstalling after an update slipped
# through) needs no download. # through) needs no download.
PINNED_KERNEL_VER="7.1.6-1" PINNED_KERNEL_VER="7.1.6-1"
@@ -148,7 +149,9 @@ remove_kernel_pin() {
RESTART_FOR_LOGIN=true RESTART_FOR_LOGIN=true
} }
# "Pin the kernel" menu item, a sub-option of Steam Machine support. # "Pin the kernel" menu item, a sub-option of Steam Machine support. The
# power-off fix made it unnecessary: only shown while on, to remove it.
kpin_available() { detect_valve_fremont && kpin_status; }
kpin_status() { kpin_status() {
pinned_kernel_installed && grep -Eq '^IgnorePkg\s*=.*\slinux-cachyos(\s|$)' /etc/pacman.conf pinned_kernel_installed && grep -Eq '^IgnorePkg\s*=.*\slinux-cachyos(\s|$)' /etc/pacman.conf
} }
@@ -163,7 +166,12 @@ kpin_enable() {
fi fi
} }
kpin_disable() { remove_kernel_pin; } kpin_disable() {
# Saved HDMI refresh boost EDIDs were for the pinned kernel only (newer
# ones read the whole EDID), also those of displays not connected now.
hdmi_status && hdmi_forget_all
remove_kernel_pin
}
install_kernel_headers() { install_kernel_headers() {
# DKMS can only build leds-valve against kernels whose headers are # DKMS can only build leds-valve against kernels whose headers are
@@ -216,9 +224,11 @@ install_valve_led_driver() {
# a clang-built tree). DKMS reads this override after the package's # a clang-built tree). DKMS reads this override after the package's
# dkms.conf; written before the install so its own build works too. # dkms.conf; written before the install so its own build works too.
# On a fresh system dkms isn't installed yet, so /etc/dkms doesn't exist. # On a fresh system dkms isn't installed yet, so /etc/dkms doesn't exist.
# Someone's own override is backed up and kept, with ours after it: it's
# sourced as bash, so our MAKE[0] wins.
sudo mkdir -p "$(dirname "$LED_DKMS_OVERRIDE")" sudo mkdir -p "$(dirname "$LED_DKMS_OVERRIDE")"
if ! printf '%s\n' "# Written by cachyos-gamescope-boot: build for DKMS's target kernel." \ if ! led_dkms_override | sudo tee "$LED_DKMS_OVERRIDE.new" >/dev/null ||
'MAKE[0]="make KVERSION=${kernelver}"' | sudo tee "$LED_DKMS_OVERRIDE" >/dev/null; then ! sudo mv -f "$LED_DKMS_OVERRIDE.new" "$LED_DKMS_OVERRIDE"; then
err "Couldn't write $LED_DKMS_OVERRIDE; without it the LED driver only builds for the running kernel." err "Couldn't write $LED_DKMS_OVERRIDE; without it the LED driver only builds for the running kernel."
return 1 return 1
fi fi
@@ -277,7 +287,33 @@ install_valve_led_driver() {
LED_UDEV_RULE="/etc/udev/rules.d/70-valve-leds-user.rules" LED_UDEV_RULE="/etc/udev/rules.d/70-valve-leds-user.rules"
LED_DKMS_OVERRIDE="/etc/dkms/leds-valve-dkms.conf" LED_DKMS_OVERRIDE="/etc/dkms/leds-valve-dkms.conf"
HEADERS_SCRIPT="/usr/local/lib/cachyos-gamescope-boot/ensure-kernel-headers"
led_dkms_override() {
# The override to write: an existing one's own lines (backed up the first
# time), without ours from an earlier run, then ours. Before 2.5.0 the
# file was ours alone, with a different comment.
local ours
ours="$(patch_file leds-valve-dkms.conf)"
if [[ -f "$LED_DKMS_OVERRIDE" ]]; then
grep -qxF -f <(echo "$ours") "$LED_DKMS_OVERRIDE" ||
grep -q '^# Written by cachyos-gamescope-boot' "$LED_DKMS_OVERRIDE" ||
backup_file "$LED_DKMS_OVERRIDE" >&2
grep -vxF -f <(echo "$ours") "$LED_DKMS_OVERRIDE" | grep -v '^# Written by cachyos-gamescope-boot'
fi
echo "$ours"
}
led_dkms_override_remove() {
# Puts someone's own override back, or removes ours.
if [[ -f "$LED_DKMS_OVERRIDE.bak-gamescope-wizard" ]]; then
sudo mv -f "$LED_DKMS_OVERRIDE.bak-gamescope-wizard" "$LED_DKMS_OVERRIDE"
else
sudo rm -f "$LED_DKMS_OVERRIDE"
fi
}
HEADERS_SCRIPT="/usr/local/lib/steamify/ensure-kernel-headers"
# Before 2.5.0; removed when it's set up again or turned off.
HEADERS_SCRIPT_OLD="/usr/local/lib/cachyos-gamescope-boot/ensure-kernel-headers"
HEADERS_UNIT="/etc/systemd/system/ensure-kernel-headers.service" HEADERS_UNIT="/etc/systemd/system/ensure-kernel-headers.service"
install_headers_boot_check() { install_headers_boot_check() {
@@ -287,36 +323,10 @@ install_headers_boot_check() {
# So check at every boot and install missing headers, which triggers # So check at every boot and install missing headers, which triggers
# that hook and builds the driver for the new kernel. # that hook and builds the driver for the new kernel.
sudo mkdir -p "$(dirname "$HEADERS_SCRIPT")" sudo mkdir -p "$(dirname "$HEADERS_SCRIPT")"
sudo tee "$HEADERS_SCRIPT" > /dev/null << 'EOF' patch_file ensure-kernel-headers.sh | sudo tee "$HEADERS_SCRIPT" > /dev/null
#!/bin/bash
# Installed by cachyos-gamescope-boot: install missing -headers for every
# installed kernel, so DKMS builds leds-valve for it.
set -u
missing=()
for k in $(pacman -Qqo /usr/lib/modules/*/pkgbase 2>/dev/null | sort -u); do
pacman -Q "${k}-headers" >/dev/null 2>&1 && continue
pacman -Si "${k}-headers" >/dev/null 2>&1 && missing+=("${k}-headers")
done
[[ ${#missing[@]} -eq 0 ]] && exit 0
# Another pacman is running (e.g. an update): try again next boot.
[[ -e /var/lib/pacman/db.lck ]] && { echo "pacman is busy, skipping"; exit 0; }
echo "Installing missing kernel headers: ${missing[*]}"
exec pacman -S --needed --noconfirm "${missing[@]}"
EOF
sudo chmod 755 "$HEADERS_SCRIPT" sudo chmod 755 "$HEADERS_SCRIPT"
sudo tee "$HEADERS_UNIT" > /dev/null << EOF sudo rm -rf "$(dirname "$HEADERS_SCRIPT_OLD")"
[Unit] service_file ensure-kernel-headers.service HEADERS_SCRIPT="$HEADERS_SCRIPT" | sudo tee "$HEADERS_UNIT" > /dev/null
Description=Install missing kernel headers so DKMS builds leds-valve for every kernel
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=$HEADERS_SCRIPT
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable ensure-kernel-headers.service sudo systemctl enable ensure-kernel-headers.service
} }
@@ -329,13 +339,13 @@ reload_powerdevil() {
kernel_overview() { kernel_overview() {
# Per installed kernel: headers (needed to build DKMS modules), the # Per installed kernel: headers (needed to build DKMS modules), the
# in-kernel Steam controller driver, and, on a Steam Machine, whether the # in-kernel Steam controller driver, and, on a Steam Machine, whether the
# LED driver is built for it. Printed in the menu so it's easy to verify # LED driver and the power-off fix are built for it. Printed in the menu so it's easy to verify
# a kernel update or a newly added kernel got everything. # a kernel update or a newly added kernel got everything.
local mark="${c_green}yes${c_reset}" miss="${c_red}no ${c_reset}" leds=false local mark="${c_green}yes${c_reset}" miss="${c_red}no ${c_reset}" leds=false
detect_valve_fremont && command -v dkms >/dev/null 2>&1 && leds=true detect_valve_fremont && command -v dkms >/dev/null 2>&1 && leds=true
local kdir k pkg headers hid led running line local kdir k pkg headers hid led off running line
local legend="> = running; controller = Steam controller driver" local legend="> = running; controller = Steam controller driver"
[[ "$leds" == true ]] && legend+=", LEDs = LED bar driver built" [[ "$leds" == true ]] && legend+=", LEDs = LED bar driver built, power-off = power-off fix built"
echo -e " ${c_bold}Kernels${c_reset} ($legend)" echo -e " ${c_bold}Kernels${c_reset} ($legend)"
for kdir in /usr/lib/modules/*/; do for kdir in /usr/lib/modules/*/; do
k="$(basename "$kdir")" k="$(basename "$kdir")"
@@ -347,11 +357,12 @@ kernel_overview() {
line="$(printf '%-23s %-18s headers %b controller %b' "$k" "$pkg" "$headers" "$hid")" line="$(printf '%-23s %-18s headers %b controller %b' "$k" "$pkg" "$headers" "$hid")"
if [[ "$leds" == true ]]; then if [[ "$leds" == true ]]; then
led="$miss"; dkms status -k "$k" leds-valve-dkms 2>/dev/null | grep -q installed && led="$mark" led="$miss"; dkms status -k "$k" leds-valve-dkms 2>/dev/null | grep -q installed && led="$mark"
line+="$(printf ' LEDs %b' "$led")" off="$miss"; dkms status -k "$k" "$POWEROFF_DKMS_NAME" 2>/dev/null | grep -q installed && off="$mark"
line+="$(printf ' LEDs %b power-off %b' "$led" "$off")"
fi fi
echo -e " ${running}${line}" echo -e " ${running}${line}"
done done
cec_status && { echo; echo -e " $(cec_overview)"; } cec_installed && { echo; echo -e " $(cec_overview)"; }
if [[ "$leds" == true ]]; then if [[ "$leds" == true ]]; then
local loaded="$miss" nodes local loaded="$miss" nodes
lsmod | grep -q '^leds_valve' && loaded="$mark" lsmod | grep -q '^leds_valve' && loaded="$mark"
@@ -367,6 +378,49 @@ machine_status() {
pacman -Qi leds-valve-dkms-git >/dev/null 2>&1 && pacman -Qi steamos-manager >/dev/null 2>&1 pacman -Qi leds-valve-dkms-git >/dev/null 2>&1 && pacman -Qi steamos-manager >/dev/null 2>&1
} }
# Steam (steamwebhelper) reads the serial number for its System settings
# straight from DMI, which the kernel makes root-only; SteamOS makes it
# readable. A tmpfiles rule does that at every boot.
SERIAL_TMPFILES=/etc/tmpfiles.d/steamify-serial.conf
# Steam's Wi-Fi backend switch goes through steamos-manager, which reads and
# writes the backend in this file (SteamOS ships it); without it, it fails
# with "Wi-Fi backend not found in config". wpa_supplicant is what
# NetworkManager uses on CachyOS anyway. Not written when someone set a
# backend themselves.
WIFI_BACKEND_CONF=/etc/NetworkManager/conf.d/99-valve-wifi-backend.conf
wifi_backend_enable() {
[[ -f "$WIFI_BACKEND_CONF" ]] && return 0
grep -rqs '^wifi.backend=' /etc/NetworkManager/NetworkManager.conf /etc/NetworkManager/conf.d && return 0
printf '%s\n' "# Written by Steamify: Steam's Wi-Fi backend setting (steamos-manager)." \
'[device]' 'wifi.backend=wpa_supplicant' | sudo install -Dm644 /dev/stdin "$WIFI_BACKEND_CONF" ||
{ err "Writing the Wi-Fi backend setting failed."; return 1; }
state_set machine wifi_backend 1
}
wifi_backend_disable() {
[[ -n "$(state_get machine wifi_backend)" ]] || return 0
sudo rm -f "$WIFI_BACKEND_CONF"
state_clear machine
}
serial_enable() {
printf '%s\n' "# Written by Steamify: Steam shows the serial number, like on SteamOS." \
'z /sys/class/dmi/id/product_serial 0444 - - -' | sudo tee "$SERIAL_TMPFILES" > /dev/null ||
{ err "Making the serial number readable for Steam failed."; return 1; }
# The file applies at every boot; now too, unless /sys is read-only (the ISO's installer runs in a chroot).
sudo systemd-tmpfiles --create "$SERIAL_TMPFILES" 2>/dev/null ||
info "The serial number becomes readable for Steam at the next boot."
return 0
}
serial_disable() {
sudo rm -f "$SERIAL_TMPFILES"
sudo chmod 0400 /sys/class/dmi/id/product_serial 2>/dev/null
return 0
}
machine_enable() { machine_enable() {
# The pinned kernel first when it's wanted too, so DKMS builds the LED # The pinned kernel first when it's wanted too, so DKMS builds the LED
# driver for it once, instead of for the current kernel and then again. # driver for it once, instead of for the current kernel and then again.
@@ -392,9 +446,11 @@ EOF
# for hardware and controller settings; it knows the Steam Machine from its DMI data. # for hardware and controller settings; it knows the Steam Machine from its DMI data.
info "Installing hardware manager packages (steamos-manager & inputplumber)..." info "Installing hardware manager packages (steamos-manager & inputplumber)..."
sudo pacman -S --needed --noconfirm steamos-manager inputplumber || { err "Installing hardware manager packages failed."; return 1; } sudo pacman -S --needed --noconfirm steamos-manager inputplumber || { err "Installing hardware manager packages failed."; return 1; }
serial_enable || return 1
wifi_backend_enable || return 1
sudo systemctl enable --now inputplumber.service sudo systemctl enable --now inputplumber.service
sudo systemctl enable --now steamos-manager.service sudo systemctl enable --now steamos-manager.service
systemctl --user enable steamos-manager.service 2>/dev/null user_systemctl enable steamos-manager.service 2>/dev/null
# HDMI-CEC ran before this and couldn't link cecd to steamos-manager yet. # HDMI-CEC ran before this and couldn't link cecd to steamos-manager yet.
cec_status && cec_link_steamos_manager cec_status && cec_link_steamos_manager
@@ -411,10 +467,12 @@ EOF
machine_disable() { machine_disable() {
info "Removing Steam Machine support..." info "Removing Steam Machine support..."
systemctl --user disable --now steamos-manager.service 2>/dev/null user_systemctl disable --now steamos-manager.service 2>/dev/null
sudo systemctl disable --now steamos-manager.service 2>/dev/null sudo systemctl disable --now steamos-manager.service 2>/dev/null
sudo systemctl disable --now inputplumber.service 2>/dev/null sudo systemctl disable --now inputplumber.service 2>/dev/null
sudo pacman -Rns --noconfirm steamos-manager inputplumber 2>/dev/null sudo pacman -Rns --noconfirm steamos-manager inputplumber 2>/dev/null
serial_disable
wifi_backend_disable
sudo rm -f "$LED_UDEV_RULE" /etc/modules-load.d/leds-valve.conf sudo rm -f "$LED_UDEV_RULE" /etc/modules-load.d/leds-valve.conf
sudo udevadm control --reload sudo udevadm control --reload
sudo modprobe -r leds-valve 2>/dev/null sudo modprobe -r leds-valve 2>/dev/null
@@ -422,8 +480,9 @@ machine_disable() {
krevert machine krevert machine
reload_powerdevil reload_powerdevil
sudo systemctl disable ensure-kernel-headers.service 2>/dev/null sudo systemctl disable ensure-kernel-headers.service 2>/dev/null
sudo rm -f "$LED_DKMS_OVERRIDE" "$HEADERS_UNIT" "$HEADERS_SCRIPT" led_dkms_override_remove
sudo rmdir "$(dirname "$HEADERS_SCRIPT")" 2>/dev/null sudo rm -f "$HEADERS_UNIT" "$HEADERS_SCRIPT"
sudo rm -rf "$(dirname "$HEADERS_SCRIPT")" "$(dirname "$HEADERS_SCRIPT_OLD")"
sudo systemctl daemon-reload sudo systemctl daemon-reload
ok "Steam Machine support removed (the AUR helper, if installed, is kept)." ok "Steam Machine support removed (the AUR helper, if installed, is kept)."
} }
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
# "Update notifications" menu item (notify), ticked by default: a systemd
# user timer checks GitHub's newest release once a day and at each Plasma
# login. When it's newer than the last Steamify that ran here, it shows a
# notification (Open Steamify / Skip this version) and a tray icon until
# then; it never updates anything itself (patches/steamify-notifier.py).
# Sourced by steamify.sh; not meant to be run on its own.
NOTIFY_SCRIPT="$STEAMIFY_BIN/steamify-notifier"
NOTIFY_UNITS="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
NOTIFY_NAME=steamify-update-check
notify_status() {
[[ -f "$NOTIFY_SCRIPT" ]] &&
user_systemctl is-enabled -q "$NOTIFY_NAME.timer" 2>/dev/null &&
user_systemctl is-enabled -q "$NOTIFY_NAME.service" 2>/dev/null
}
notify_seen() {
# Every run records its version: the notifier compares the newest
# release with it, so opening Steamify ends a reminder. Also whether it
# was the app or the terminal menu: the notification opens the same.
notify_status || return 0
state_set notify seen "$VERSION"
if [[ "$BACKEND" == true ]]; then state_set notify frontend app; else state_set notify frontend terminal; fi
}
notify_enable() {
local p missing=()
info "Adding update notifications..."
# PySide6 for the tray icon, notify-send for the notification's buttons.
python3 -c 'import PySide6.QtWidgets' 2>/dev/null || missing+=(pyside6)
command -v notify-send >/dev/null || missing+=(libnotify)
if ((${#missing[@]})); then
sudo pacman -S --needed --noconfirm "${missing[@]}" ||
{ err "Installing ${missing[*]} failed."; return 1; }
fi
patch_file steamify-notifier.py | install_executable "$NOTIFY_SCRIPT" 755 ||
{ err "Couldn't write $NOTIFY_SCRIPT."; return 1; }
mkdir -p "$NOTIFY_UNITS"
service_file "$NOTIFY_NAME.service" NOTIFY_SCRIPT="$NOTIFY_SCRIPT" > "$NOTIFY_UNITS/$NOTIFY_NAME.service"
service_file "$NOTIFY_NAME.timer" > "$NOTIFY_UNITS/$NOTIFY_NAME.timer"
user_systemctl daemon-reload
user_systemctl enable "$NOTIFY_NAME.service" >/dev/null 2>&1 &&
user_systemctl enable --now "$NOTIFY_NAME.timer" >/dev/null 2>&1 ||
{ err "Enabling the update check failed."; return 1; }
state_set notify seen "$VERSION"
ok "Update notifications on: you'll get a notification when there's a new Steamify."
}
notify_disable() {
user_systemctl disable --now "$NOTIFY_NAME.timer" "$NOTIFY_NAME.service" >/dev/null 2>&1
rm -f "$NOTIFY_UNITS/$NOTIFY_NAME.service" "$NOTIFY_UNITS/$NOTIFY_NAME.timer" "$NOTIFY_SCRIPT"
rmdir "$STEAMIFY_BIN" 2>/dev/null || true
user_systemctl daemon-reload
state_clear notify
ok "Update notifications off."
}
+43 -10
View File
@@ -54,9 +54,17 @@ theme_enable() {
# Only remove it again on disable if we were the ones installing it. # Only remove it again on disable if we were the ones installing it.
state_set theme installed_pkg 1 state_set theme installed_pkg 1
fi fi
# Without a session (the ISO's installer), for a user who has never
# logged in: the layout still CachyOS's default from /etc/skel is
# removed (after the backup below), and Plasma builds its first layout
# from the look-and-feel package, as --resetLayout does below.
local offline=false layout=~/.config/plasma-org.kde.plasma.desktop-appletsrc
if [[ -z "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]]; then if [[ -z "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]]; then
err "Run this from the Plasma desktop: applying the Vapor layout needs a running Plasma session." if [[ -e "$layout" ]] && ! cmp -s "$layout" "/etc/skel/.config/${layout##*/}"; then
return 1 err "Run this from the Plasma desktop: applying the Vapor layout needs a running Plasma session."
return 1
fi
offline=true
fi fi
# Only the first time, so a re-apply doesn't overwrite the originals. # Only the first time, so a re-apply doesn't overwrite the originals.
@@ -87,22 +95,43 @@ theme_enable() {
kset theme kdeglobals General ColorScheme --delete kset theme kdeglobals General ColorScheme --delete
plasma-apply-colorscheme Vapor >/dev/null 2>&1 || true plasma-apply-colorscheme Vapor >/dev/null 2>&1 || true
if [[ "$offline" == true ]]; then
rm -f ~/.config/plasma-org.kde.plasma.desktop-appletsrc ~/.config/plasmashellrc
extras_enable || warn "SteamOS desktop extras not installed (see above); the theme itself is on."
ok "Vapor theme set; Plasma lays out the Vapor desktop at the first login."
return 0
fi
info "Applying the Vapor theme with its desktop and window layout..." info "Applying the Vapor theme with its desktop and window layout..."
# --resetLayout is "Desktop and window layout" in System Settings: the # --resetLayout is "Desktop and window layout" in System Settings: the
# SteamOS panel, launcher icon and wallpaper. It needs plasmashell up. # SteamOS panel, launcher icon and wallpaper. It needs plasmashell up.
lookandfeeltool -a "$VAPOR_LOOKANDFEEL" --resetLayout >/dev/null 2>&1 || lookandfeeltool -a "$VAPOR_LOOKANDFEEL" --resetLayout >/dev/null 2>&1 ||
{ err "lookandfeeltool failed."; return 1; } { err "lookandfeeltool failed."; return 1; }
# The new layout has a new launcher, without single user's settings. # The new layout has a new launcher, without single user's settings, and the default taskbar pins.
if single_status; then sleep 3
sleep 3 stop_plasmashell_for_edit
stop_plasmashell_for_edit taskbar_drop_discover
single_launcher single_status && single_launcher
restart_plasmashell_if_stopped restart_plasmashell_if_stopped
fi
extras_enable || warn "SteamOS desktop extras not installed (see above); the theme itself is on." extras_enable || warn "SteamOS desktop extras not installed (see above); the theme itself is on."
ok "Vapor theme active." ok "Vapor theme active."
} }
taskbar_drop_discover() {
# Plasma's default taskbar pins Discover, which CachyOS doesn't install: the pin shows as a
# blank icon. Pin what CachyOS's own layout pins (/etc/skel) instead. Not when Discover is
# installed, and only on a taskbar that still has the default pins (no launchers key).
# Edit only while plasmashell is stopped.
[[ -e /usr/share/applications/org.kde.discover.desktop ]] && return 0
local applet grp
for applet in $(plasma_applets org.kde.plasma.icontasks); do
grp="Containments|${applet%%:*}|Applets|${applet#*:}|Configuration|General"
[[ -n "$(kreadconfig6 --file plasma-org.kde.plasma.desktop-appletsrc --group Containments --group "${applet%%:*}" \
--group Applets --group "${applet#*:}" --group Configuration --group General --key launchers)" ]] && continue
kset theme plasma-org.kde.plasma.desktop-appletsrc "$grp" launchers \
'applications:systemsettings.desktop,preferred://filemanager,preferred://browser'
done
}
theme_disable() { theme_disable() {
extras_disable extras_disable
info "Restoring the previous desktop look and layout..." info "Restoring the previous desktop look and layout..."
@@ -120,7 +149,11 @@ theme_disable() {
fi fi
# After the tools above, so keys that weren't set before are removed again. # After the tools above, so keys that weren't set before are removed again.
krevert theme krevert theme
single_status && single_launcher if single_status; then
single_launcher
else
single_launcher_drop
fi
restart_plasmashell_if_stopped restart_plasmashell_if_stopped
qdbus6 org.kde.KWin /KWin org.kde.KWin.reconfigure >/dev/null 2>&1 || true qdbus6 org.kde.KWin /KWin org.kde.KWin.reconfigure >/dev/null 2>&1 || true
+143
View File
@@ -0,0 +1,143 @@
#!/bin/bash
# "VRAM booster" menu item (vram), ticked by default, like SteamOS 3.9's
# dGPU VRAM management: the game in front keeps its VRAM, background apps
# are evicted first, so a game that needs most of it doesn't spill into
# system RAM and stutter. The kernel side is the dmem cgroup controller
# (7.2; amdgpu and xe register their VRAM, NVIDIA's driver doesn't), so it's
# only offered when the kernel lists a VRAM region (vram or vidmem) of at least 2 GB. CachyOS packages the userspace
# side: dmemcg-booster (a system service that enables the controller and
# sets the limits, plus a user service) and plasma-foreground-booster, which
# tells it which window is in front on the desktop. The latter only starts
# with "autostart" set in kcgroupsrc.
# Sourced by steamify.sh; not meant to be run on its own.
VRAM_PKGS=(dmemcg-booster plasma-foreground-booster)
VRAM_MIN_BYTES=$((2 * 1024 * 1024 * 1024))
# The regions the kernel's dmem controller lists (a file under /sys/fs/cgroup;
# a variable so tests can point it at a copy).
VRAM_CAPACITY="${WIZARD_VRAM_CAPACITY:-/sys/fs/cgroup/dmem.capacity}"
vram_supported() {
# WIZARD_VRAM_FAKE_NVIDIA (tests): as with an NVIDIA card whose driver
# doesn't register its VRAM; see vram_nvidia_case.
[[ -n "${WIZARD_VRAM_FAKE_NVIDIA:-}" ]] && return 1
# Only for a GPU with its own VRAM: an integrated GPU registers a small
# carve-out too, but mostly uses system RAM, so there's little to boost.
# By what the driver registers, not the brand: amdgpu and xe name the
# region "vram", other drivers "vidmem" (numbered with several), so an
# NVIDIA driver that registers its memory is offered right away.
awk -v min="$VRAM_MIN_BYTES" '$1 ~ /\/(vram|vidmem)[0-9]*$/ && $2 >= min { found = 1 } END { exit !found }' \
"$VRAM_CAPACITY" 2>/dev/null
}
vram_nvidia() {
# An NVIDIA display controller (PCI vendor 10de, class 03xxxx).
local d
[[ -n "${WIZARD_VRAM_FAKE_NVIDIA:-}" ]] && return 0
for d in /sys/bus/pci/devices/*; do
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de && "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] && return 0
done
return 1
}
# chwd's lists of NVIDIA cards that need a closed legacy branch (580xx,
# 470xx, 390xx); every other NVIDIA card runs the open kernel modules.
VRAM_CHWD_IDS=/var/lib/chwd/ids
vram_nvidia_legacy_id() {
# vram_nvidia_legacy_id <pci device id, with or without 0x>: 0 when chwd
# lists the card for a closed legacy branch, i.e. it is older than the
# RTX 20 series. Also what lib/nvidia.sh uses to decide if a card is supported.
grep -qwi "${1#0x}" "$VRAM_CHWD_IDS"/nvidia-*.ids 2>/dev/null
}
vram_nvidia_case() {
# Why an NVIDIA card's VRAM isn't registered, so the menu can say what to
# do: "update" (open modules older than 615), "switch <chwd profile>"
# (closed driver on a card the open one supports), "legacy" (card only
# runs a closed legacy branch), "nouveau" (no NVIDIA module).
# WIZARD_VRAM_FAKE_NVIDIA=1 fakes "switch nvidia-dkms-580xx", or name a case.
local fake="${WIZARD_VRAM_FAKE_NVIDIA:-}" license d id p
if [[ -n "$fake" ]]; then
[[ "$fake" == 1 ]] && fake="switch nvidia-dkms-580xx"
echo "$fake"; return
fi
license="$(modinfo -F license nvidia 2>/dev/null)"
case "$license" in
"Dual MIT/GPL") echo update; return ;;
"") echo nouveau; return ;;
esac
for d in /sys/bus/pci/devices/*; do
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de && "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] || continue
vram_nvidia_legacy_id "$(cat "$d/device")" && { echo legacy; return; }
done
for p in 580xx 470xx 390xx; do
pacman -Q "nvidia-$p-dkms" >/dev/null 2>&1 && { echo "switch nvidia-dkms-$p"; return; }
done
echo switch
}
vram_nvidia_hint() {
# vram_nvidia_hint <case>: the short reason (terminal menu, the app's row).
case "$1" in
update) echo "update NVIDIA's driver to 615 or newer (sudo pacman -Syu)" ;;
switch*) echo "needs NVIDIA's open driver, which your card supports" ;;
legacy) echo "your NVIDIA card's driver doesn't support it (older than RTX 20)" ;;
*) echo "the nouveau driver doesn't support it yet" ;;
esac
}
vram_nvidia_note() {
# vram_nvidia_note <case>: the full explanation, with what to do.
local profile="${1#switch}"; profile="${profile# }"
case "$1" in
update) echo "NVIDIA's open driver tells Linux how its video memory is used from version 615 on; yours is older ($(cat /sys/module/nvidia/version 2>/dev/null || echo unknown)). Update your system (sudo pacman -Syu), restart, and it's available here." ;;
switch*) echo "Your card also runs NVIDIA's open driver, the one CachyOS installs by default, and only that one tells Linux how its video memory is used. You're using NVIDIA's closed driver. Switch in a terminal with: ${profile:+sudo chwd -r $profile && }sudo chwd -i nvidia-open-dkms, then restart: it's available here after that. Steamify doesn't switch it for you: if something goes wrong, the screen stays black after the restart." ;;
legacy) echo "Your NVIDIA card is older than the RTX 20 series and only runs NVIDIA's closed driver, which doesn't tell Linux how its video memory is used. Newer NVIDIA cards (RTX 20 series and up), and AMD and Intel graphics cards, support it." ;;
*) echo "The open-source nouveau driver doesn't tell Linux how its video memory is used yet (a patch is on its way into the kernel). NVIDIA's own open driver does, from version 615 on (sudo chwd -a installs the right one)." ;;
esac
}
# Shown where it works, and greyed out with an NVIDIA card whose driver
# doesn't register its VRAM with the kernel (yet): so it's clear why it
# can't be turned on. Hidden elsewhere (integrated graphics, older kernel).
vram_available() { vram_supported || vram_status || vram_nvidia; }
vram_selectable() { vram_supported || vram_status; }
vram_status() {
pacman -Q "${VRAM_PKGS[@]}" >/dev/null 2>&1 &&
systemctl is-enabled -q dmemcg-booster-system.service 2>/dev/null &&
user_systemctl is-enabled -q dmemcg-booster-user.service 2>/dev/null
}
vram_enable() {
local p missing=()
info "Installing the VRAM booster (the game in front keeps its VRAM)..."
for p in "${VRAM_PKGS[@]}"; do pacman -Q "$p" >/dev/null 2>&1 || missing+=("$p"); done
if ((${#missing[@]})); then
sudo pacman -S --needed --noconfirm "${missing[@]}" ||
{ err "Installing ${missing[*]} failed."; return 1; }
state_set vram installed_pkgs "${missing[*]}"
fi
sudo systemctl enable --now dmemcg-booster-system.service >/dev/null 2>&1 ||
{ err "Starting dmemcg-booster failed."; return 1; }
user_systemctl enable --now dmemcg-booster-user.service >/dev/null 2>&1 ||
{ err "Starting dmemcg-booster for $USER failed."; return 1; }
kset vram kcgroupsrc "Foreground Booster" autostart true
# Started by the Plasma session from now on; start it in this one too.
[[ "${XDG_CURRENT_DESKTOP:-}" == KDE ]] &&
user_systemctl start plasma-foreground-booster.service >/dev/null 2>&1
ok "VRAM booster on: the game in front keeps its VRAM."
}
vram_disable() {
local pkgs
user_systemctl disable --now plasma-foreground-booster.service dmemcg-booster-user.service >/dev/null 2>&1
sudo systemctl disable --now dmemcg-booster-system.service >/dev/null 2>&1
krevert vram
pkgs="$(state_get vram installed_pkgs)"
# shellcheck disable=SC2086 # a list of package names
[[ -n "$pkgs" ]] && sudo pacman -Rns --noconfirm $pkgs >/dev/null 2>&1
state_clear vram
ok "VRAM booster off."
}
+64 -17
View File
@@ -1,19 +1,29 @@
#!/bin/bash #!/bin/bash
# "Steamify shortcut" menu item: a desktop icon and an app launcher entry that # "Steamify shortcut" menu item: "Steamify CachyOS" on the desktop and in the
# open the newest release of Steamify CachyOS in Konsole (curl | bash, like the # app launcher opens the newest release of the app (curl | bash of
# install command in the README), so it's never out of date. # steamify-app.sh, like the install command in the README), so it's never out
# of date; "Steamify Terminal" in the launcher opens the terminal menu in
# Konsole the same way.
# Sourced by steamify.sh; not meant to be run on its own. # Sourced by steamify.sh; not meant to be run on its own.
WIZARD_RELEASE="https://github.com/theupriser/steamify-cachyos/releases/latest/download" WIZARD_RELEASE="https://github.com/theupriser/steamify-cachyos/releases/latest/download"
WIZARD_URL="$WIZARD_RELEASE/steamify.sh" WIZARD_URL="$WIZARD_RELEASE/steamify.sh"
WIZARD_APP_URL="$WIZARD_RELEASE/steamify-app.sh"
# Steam logo with a gear (assets/ in the repo, published with every release). # Steam logo with a gear (assets/ in the repo, published with every release).
WIZARD_ICON="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps/cachyos-gamescope-boot-wizard.svg" WIZARD_ICON="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps/steamify.svg"
WIZARD_LAUNCHER="${XDG_DATA_HOME:-$HOME/.local/share}/cachyos-gamescope-boot/run-wizard" WIZARD_LAUNCHER="$STEAMIFY_BIN/run-wizard"
WIZARD_DESKTOP_NAME="cachyos-gamescope-boot-wizard.desktop" WIZARD_APP_LAUNCHER="$STEAMIFY_BIN/run-app"
WIZARD_APP_ENTRY="${XDG_DATA_HOME:-$HOME/.local/share}/applications/$WIZARD_DESKTOP_NAME" WIZARD_APPS="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
# steamify-ui: the id the app gives itself, so its window belongs to this
# entry. steamify-app.sh leaves an entry with X-Steamify-Shortcut alone.
WIZARD_DESKTOP_NAME="steamify-ui.desktop"
WIZARD_APP_ENTRY="$WIZARD_APPS/$WIZARD_DESKTOP_NAME"
WIZARD_TERMINAL_ENTRY="$WIZARD_APPS/steamify-terminal.desktop"
# Before 2.0.1 the shortcut opened the terminal menu under this name.
WIZARD_OLD_NAME="cachyos-gamescope-boot-wizard.desktop"
wizard_desktop_file() { wizard_desktop_dir() {
echo "$(xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop")/$WIZARD_DESKTOP_NAME" xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop"
} }
install_executable() { install_executable() {
@@ -29,11 +39,15 @@ install_executable() {
} }
launcher_status() { launcher_status() {
[[ -x "$WIZARD_LAUNCHER" && -f "$WIZARD_APP_ENTRY" ]] [[ -x "$WIZARD_APP_LAUNCHER" && -x "$WIZARD_LAUNCHER" && -f "$WIZARD_APP_ENTRY" && -f "$WIZARD_TERMINAL_ENTRY" ]]
} }
# A shortcut from before 2.0.1 (terminal only): the menu ticks the item, so a
# normal run replaces it.
launcher_repair() { [[ -f "$WIZARD_APPS/$WIZARD_OLD_NAME" ]] && ! launcher_status; }
launcher_enable() { launcher_enable() {
info "Adding the Steamify shortcut (desktop icon and app launcher entry)..." info "Adding the Steamify shortcut (the app on the desktop and in the launcher, Steamify Terminal in the launcher)..."
install_executable "$WIZARD_LAUNCHER" 755 << EOF install_executable "$WIZARD_LAUNCHER" 755 << EOF
#!/bin/bash #!/bin/bash
# Installed by Steamify CachyOS: run its newest release. # Installed by Steamify CachyOS: run its newest release.
@@ -53,6 +67,23 @@ for (( i = 10; i > 0; i-- )); do
read -rs -t 1 _ && break read -rs -t 1 _ && break
done done
echo echo
EOF
# The app, without a terminal. Only the first time, when PySide6 still
# has to be installed, it runs in Konsole: sudo asks for the password there.
install_executable "$WIZARD_APP_LAUNCHER" 755 << EOF
#!/bin/bash
# Installed by Steamify CachyOS: start the newest release of its app.
set -o pipefail
if [[ "\${1:-}" != --in-terminal ]] && ! python3 -c 'import PySide6.QtQml' 2>/dev/null; then
exec konsole -e "\$0" --in-terminal
fi
if ! curl -fsSL --max-time 30 "$WIZARD_APP_URL" | bash; then
msg="Couldn't download or start Steamify. Are you connected to the internet?"
if [[ "\${1:-}" == --in-terminal ]]; then echo; echo "\$msg"; read -rp "Press Enter to close this window... " _
else kdialog --title Steamify --sorry "\$msg" 2>/dev/null; fi
exit 1
fi
EOF EOF
# The icon comes with the release; without it, Steam's own icon. # The icon comes with the release; without it, Steam's own icon.
@@ -60,7 +91,7 @@ EOF
tmp="$(mktemp)" tmp="$(mktemp)"
if curl -fsSL --max-time 20 "$WIZARD_RELEASE/steam-gaming-settings.svg" -o "$tmp" && grep -q '<svg' "$tmp"; then if curl -fsSL --max-time 20 "$WIZARD_RELEASE/steam-gaming-settings.svg" -o "$tmp" && grep -q '<svg' "$tmp"; then
install -D -m 644 "$tmp" "$WIZARD_ICON" install -D -m 644 "$tmp" "$WIZARD_ICON"
icon=cachyos-gamescope-boot-wizard icon=steamify
gtk-update-icon-cache -q -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || true gtk-update-icon-cache -q -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || true
else else
warn "Couldn't download the shortcut's icon; using Steam's icon instead." warn "Couldn't download the shortcut's icon; using Steam's icon instead."
@@ -72,18 +103,34 @@ EOF
Type=Application Type=Application
Name=Steamify CachyOS Name=Steamify CachyOS
Comment=Turn the SteamOS-style parts of this PC on or off Comment=Turn the SteamOS-style parts of this PC on or off
Exec=$WIZARD_APP_LAUNCHER
Icon=$icon
Terminal=false
Categories=Game;System;Settings;
X-Steamify-Shortcut=true"
printf '%s\n' "$entry" | install_executable "$WIZARD_APP_ENTRY" 644
printf '%s\n' "$entry" | install_executable "$(wizard_desktop_dir)/$WIZARD_DESKTOP_NAME" 755
printf '%s\n' "[Desktop Entry]
Type=Application
Name=Steamify Terminal
Comment=Steamify CachyOS's menu in a terminal
Exec=konsole -e $WIZARD_LAUNCHER Exec=konsole -e $WIZARD_LAUNCHER
Icon=$icon Icon=$icon
Terminal=false Terminal=false
Categories=Game;System;Settings;" Categories=Game;System;Settings;" | install_executable "$WIZARD_TERMINAL_ENTRY" 644
printf '%s\n' "$entry" | install_executable "$WIZARD_APP_ENTRY" 644 # The terminal-only shortcut from before 2.0.1.
printf '%s\n' "$entry" | install_executable "$(wizard_desktop_file)" 755 rm -f "$(wizard_desktop_dir)/$WIZARD_OLD_NAME" "$WIZARD_APPS/$WIZARD_OLD_NAME"
kbuildsycoca6 >/dev/null 2>&1 || true kbuildsycoca6 >/dev/null 2>&1 || true
ok "Steamify shortcut added: \"Steamify CachyOS\" on the desktop and in the launcher." ok "Steamify shortcut added: \"Steamify CachyOS\" (the app) on the desktop and in the launcher, \"Steamify Terminal\" in the launcher."
} }
launcher_disable() { launcher_disable() {
rm -f "$(wizard_desktop_file)" "$WIZARD_APP_ENTRY" "$WIZARD_LAUNCHER" "$WIZARD_ICON" # The launcher entry only if it's ours: steamify-app.sh writes its own
# under the same name when there's no shortcut.
grep -q '^X-Steamify-Shortcut=true' "$WIZARD_APP_ENTRY" 2>/dev/null && rm -f "$WIZARD_APP_ENTRY"
rm -f "$(wizard_desktop_dir)/$WIZARD_DESKTOP_NAME" "$WIZARD_TERMINAL_ENTRY" \
"$(wizard_desktop_dir)/$WIZARD_OLD_NAME" "$WIZARD_APPS/$WIZARD_OLD_NAME" \
"$WIZARD_LAUNCHER" "$WIZARD_APP_LAUNCHER" "$WIZARD_ICON"
rmdir "$(dirname "$WIZARD_LAUNCHER")" 2>/dev/null || true rmdir "$(dirname "$WIZARD_LAUNCHER")" 2>/dev/null || true
kbuildsycoca6 >/dev/null 2>&1 || true kbuildsycoca6 >/dev/null 2>&1 || true
ok "Steamify shortcut removed." ok "Steamify shortcut removed."
+18
View File
@@ -0,0 +1,18 @@
# Patches and extra sources
Files Steamify builds or applies, kept out of the shell code so they can be
read and reviewed on their own. `lib/*.sh` reads them with `patch_file
<name>` (piped through `fill KEY=value...` for `@KEY@` placeholders); the single-file build (`.github/tools/bundle.sh`) embeds them.
| File | Used by | What |
|---|---|---|
| `steamify-fremont-poweroff.c` | `lib/fremont-poweroff.sh` (Steam Machine support) | Kernel module (DKMS): clears GPIO 18's S4/S5 wake bit before power-off, so the Steam Machine stays off with recent kernels |
| `sync-steamos-session.sh` | `lib/login-manager.sh` (SteamOS conversion, plasma-login-manager) | The sync bridge: copies Steam's session choice from `zz-steamos-autologin.conf` into `plasmalogin.conf`'s `[Autologin]` |
| `ensure-kernel-headers.sh` | `lib/steam-machine.sh` (Steam Machine support) | Run at boot: installs missing headers for every installed kernel |
| `leds-valve-dkms.conf` | `lib/steam-machine.sh` (Steam Machine support) | `/etc/dkms` override: build the LED driver for DKMS's target kernel; appended to someone's own override, which is backed up and restored |
| `steamify-fremont-poweroff.dkms.conf`, `steamify-cros-ec-cec.dkms.conf` | `lib/fremont-poweroff.sh`, `lib/cec.sh` | The DKMS modules' `dkms.conf` (`fill NAME=... VERSION=...`) |
| `steam-shortcuts.py` | `lib/steam-game.sh` (Add as non-Steam game) | Reads and writes Steam's binary `shortcuts.vdf` (find, add or update, remove) |
| `steamify-notifier.py` | `lib/update-notifier.sh` (Update notifications) | The daily check: notification and tray icon when there's a new release |
| `steamify-os-release.sh`, `steamify-os-release.hook` | `lib/login-manager.sh` (SteamOS conversion) | What Steam's System settings show besides the OS name (which stays CachyOS's): `VERSION_ID=steamos-X.Y`, `VERSION_CODENAME=steam-machine`, `VARIANT`/`VARIANT_ID` Steamify with its version, in `/etc/os-release`; the pacman hook runs it again after cachyos-hooks (`fill SCRIPT=... VERSION=... STEAMOS=...`) |
| `steamify-nvidia-initramfs.sh`, `steamify-nvidia-initramfs.hook` | `lib/nvidia.sh` (SteamOS conversion, NVIDIA fix) | The pacman hook (before the initramfs is built, after DKMS) and its script: the NVIDIA modules go into the initramfs only while every installed kernel has them, decided again at every kernel or driver change (`fill SCRIPT=...`) |
| `cros-ec-cec-single-port.patch` | `lib/cec.sh` (HDMI-CEC) | Makes Valve's Steam Machine CEC driver find amdgpu's HDMI port |
+21
View File
@@ -0,0 +1,21 @@
Steam Machine CEC: find amdgpu's HDMI port with a single CEC port.
amdgpu registers its HDMI notifier without a port name, which only matches
the driver's named lookup ("Port C") if the CEC driver loaded first; amdgpu
loads from the initramfs, so it never does and /dev/cec0 stays at f.f.f.f.
With a single CEC port, look the notifier up by device alone.
Applies to Valve's drivers/media/cec/platform/cros-ec/cros-ec-cec.c
(evlaV/linux-integration 10c8c880, see CEC_DRIVER_URL in lib/cec.sh).
--- a/cros-ec-cec.c
+++ b/cros-ec-cec.c
@@ -485,7 +485,7 @@
goto out_probe_adapter;
}
- port->notify = cec_notifier_cec_adap_register(hdmi_dev, conns[port_num],
+ port->notify = cec_notifier_cec_adap_register(hdmi_dev, conns[1] ? conns[port_num] : NULL,
port->adap);
if (!port->notify) {
ret = -ENOMEM;
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# Installed by Steamify: install missing -headers for every
# installed kernel, so DKMS builds leds-valve for it.
set -u
missing=()
for k in $(pacman -Qqo /usr/lib/modules/*/pkgbase 2>/dev/null | sort -u); do
pacman -Q "${k}-headers" >/dev/null 2>&1 && continue
pacman -Si "${k}-headers" >/dev/null 2>&1 && missing+=("${k}-headers")
done
[[ ${#missing[@]} -eq 0 ]] && exit 0
# Another pacman is running (e.g. an update): try again next boot.
[[ -e /var/lib/pacman/db.lck ]] && { echo "pacman is busy, skipping"; exit 0; }
echo "Installing missing kernel headers: ${missing[*]}"
exec pacman -S --needed --noconfirm "${missing[@]}"
+2
View File
@@ -0,0 +1,2 @@
# Written by Steamify: build leds-valve for DKMS's target kernel, not the running one.
MAKE[0]="make KVERSION=${kernelver}"
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
# Steamify CachyOS: Steam's non-Steam games list (userdata/<id>/config/
# shortcuts.vdf, binary VDF), for "Add as non-Steam game". Steam reads it only
# at startup and writes it back on exit, so it's edited while Steam is closed.
# steam-shortcuts.py find <vdf> <exe> -> appid of an entry for exe
# steam-shortcuts.py add <vdf> <name> <exe> <icon> <desktop file>
# -> "added <appid>" / "updated <appid>"
# steam-shortcuts.py remove <vdf> <appid>
# An entry counts as ours when its Exe is <exe>, or a Steamify start script
# (run-app, also under the pre-2.5.0 path): that one is updated, not doubled.
import os
import struct
import sys
import zlib
MAP, STR, INT, END = 0, 1, 2, 8
def parse(data):
pos = 0
def cstr():
nonlocal pos
end = data.index(b"\0", pos)
s = data[pos:end].decode("utf-8", "surrogateescape")
pos = end + 1
return s
def block():
nonlocal pos
items = []
while True:
t = data[pos]
pos += 1
if t == END:
return items
key = cstr()
if t == MAP:
items.append((key, block()))
elif t == STR:
items.append((key, cstr()))
elif t == INT:
items.append((key, struct.unpack_from("<I", data, pos)[0]))
pos += 4
else:
raise ValueError(f"unknown VDF type {t} at {pos}")
if not data:
return [("shortcuts", [])]
root = block()
return root
def dump(items):
out = bytearray()
for key, value in items:
k = key.encode("utf-8", "surrogateescape") + b"\0"
if isinstance(value, list):
out += bytes([MAP]) + k + dump(value)
elif isinstance(value, int):
out += bytes([INT]) + k + struct.pack("<I", value & 0xFFFFFFFF)
else:
out += bytes([STR]) + k + value.encode("utf-8", "surrogateescape") + b"\0"
return bytes(out) + bytes([END])
def get(entry, key, default=None):
# Steam's key names vary in case between versions (appid/AppName/Exe).
for k, v in entry:
if k.lower() == key.lower():
return v
return default
def put(entry, key, value):
for i, (k, _) in enumerate(entry):
if k.lower() == key.lower():
entry[i] = (k, value)
return
entry.append((key, value))
def shortcuts(root):
for k, v in root:
if k.lower() == "shortcuts":
return v
raise ValueError("no shortcuts block")
def ours(entry, exe):
have = get(entry, "Exe", "").strip('"')
return have == exe or (have.endswith("/run-app") and
("/steamify/" in have or "/cachyos-gamescope-boot/" in have))
def load(path):
try:
with open(path, "rb") as f:
return parse(f.read())
except FileNotFoundError:
return [("shortcuts", [])]
def save(path, root):
tmp = path + ".steamify-tmp"
with open(tmp, "wb") as f:
f.write(dump(root))
os.replace(tmp, path)
def main(argv):
cmd, path = argv[1], argv[2]
root = load(path)
entries = shortcuts(root)
if cmd == "find":
for _, e in entries:
if ours(e, argv[3]):
print(get(e, "appid", 0))
return 0
return 1
if cmd == "add":
name, exe, icon, desktop = argv[3:7]
for _, e in entries:
if ours(e, exe):
put(e, "AppName", name)
put(e, "Exe", f'"{exe}"')
put(e, "StartDir", f'"{os.path.dirname(exe)}/"')
put(e, "icon", icon)
put(e, "ShortcutPath", desktop)
save(path, root)
print("updated", get(e, "appid", 0))
return 0
# Steam's own id for a shortcut: CRC32 of exe and name, top bit set.
appid = (zlib.crc32(f'"{exe}"{name}'.encode()) | 0x80000000) & 0xFFFFFFFF
entry = [("appid", appid), ("AppName", name), ("Exe", f'"{exe}"'),
("StartDir", f'"{os.path.dirname(exe)}/"'), ("icon", icon),
("ShortcutPath", desktop), ("LaunchOptions", ""), ("IsHidden", 0),
("AllowDesktopConfig", 1), ("AllowOverlay", 1), ("OpenVR", 0),
("Devkit", 0), ("DevkitGameID", ""), ("DevkitOverrideAppID", 0),
("LastPlayTime", 0), ("FlatpakAppID", ""), ("tags", [])]
entries.append((str(len(entries)), entry))
if os.path.dirname(path):
os.makedirs(os.path.dirname(path), exist_ok=True)
save(path, root)
print("added", appid)
return 0
if cmd == "remove":
keep = [e for _, e in entries if str(get(e, "appid", "")) != argv[3]]
if len(keep) == len(entries):
return 0
entries[:] = [(str(i), e) for i, e in enumerate(keep)]
save(path, root)
return 0
print(f"unknown command {cmd}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main(sys.argv))
+6
View File
@@ -0,0 +1,6 @@
# Written by Steamify: Valve's cros_ec_cec, which knows the Steam Machine.
PACKAGE_NAME="@NAME@"
PACKAGE_VERSION="@VERSION@"
BUILT_MODULE_NAME[0]="cros-ec-cec"
DEST_MODULE_LOCATION[0]="/updates/dkms"
AUTOINSTALL="yes"
+80
View File
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Steam Machine (Fremont): power off instead of booting up again.
*
* Since "pinctrl-amd: Don't clear S4 wake bits at probe" (Linux 7.2, also
* backported to 6.x, 7.0 and 7.1 updates) the S4/S5 wake bit the firmware
* leaves set on GPIO pin 18 (_SB.PCI0.GPP6) stays set, and the machine starts again right after powering off.
* Valve's kernel clears that bit at probe on Fremont (not for upstream);
* this does the same right before power-off, for kernels without it.
*/
#include <linux/acpi.h>
#include <linux/bits.h>
#include <linux/dmi.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/reboot.h>
#define FREMONT_GPP6_PIN 18
#define WAKE_CNTRL_OFF_S4 15
static void __iomem *pin_reg;
static struct sys_off_handler *handler;
static int fremont_poweroff_prepare(struct sys_off_data *data)
{
u32 v = readl(pin_reg);
if (v & BIT(WAKE_CNTRL_OFF_S4))
writel(v & ~BIT(WAKE_CNTRL_OFF_S4), pin_reg);
return NOTIFY_DONE;
}
static int __init fremont_poweroff_init(void)
{
struct acpi_device *adev;
struct device *dev;
struct resource *res;
u32 v;
if (!dmi_match(DMI_BOARD_NAME, "Fremont"))
return -ENODEV;
adev = acpi_dev_get_first_match_dev("AMDI0030", NULL, -1);
if (!adev)
return -ENODEV;
dev = bus_find_device_by_acpi_dev(&platform_bus_type, adev);
acpi_dev_put(adev);
if (!dev)
return -ENODEV;
res = platform_get_resource(to_platform_device(dev), IORESOURCE_MEM, 0);
put_device(dev);
if (!res || resource_size(res) < (FREMONT_GPP6_PIN + 1) * 4)
return -ENODEV;
/* pinctrl-amd owns the region; only this one register is touched. */
pin_reg = ioremap(res->start + FREMONT_GPP6_PIN * 4, 4);
if (!pin_reg)
return -ENOMEM;
v = readl(pin_reg);
pr_info("GPIO %d register 0x%08x, S4/S5 wake %s\n", FREMONT_GPP6_PIN, v,
v & BIT(WAKE_CNTRL_OFF_S4) ? "set (cleared at power-off)" : "clear");
handler = register_sys_off_handler(SYS_OFF_MODE_POWER_OFF_PREPARE,
SYS_OFF_PRIO_DEFAULT, fremont_poweroff_prepare, NULL);
if (IS_ERR(handler)) {
iounmap(pin_reg);
return PTR_ERR(handler);
}
return 0;
}
static void __exit fremont_poweroff_exit(void)
{
unregister_sys_off_handler(handler);
iounmap(pin_reg);
}
module_init(fremont_poweroff_init);
module_exit(fremont_poweroff_exit);
MODULE_DESCRIPTION("Steam Machine: clear GPIO 18's S4 wake bit before power-off");
MODULE_LICENSE("GPL");
MODULE_ALIAS("dmi:*:rnFremont:*");
@@ -0,0 +1,6 @@
# Written by Steamify: the Steam Machine powers off instead of booting up again.
PACKAGE_NAME="@NAME@"
PACKAGE_VERSION="@VERSION@"
BUILT_MODULE_NAME[0]="@NAME@"
DEST_MODULE_LOCATION[0]="/updates/dkms"
AUTOINSTALL="yes"
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
# Steamify CachyOS: update notifications (installed by the "Update
# notifications" menu item, run by a systemd user timer and at each Plasma
# login). Never updates anything itself: when GitHub has a newer release
# than the last Steamify that ran here, it shows a notification (Open
# Steamify / Skip this version) and a tray icon until you open the app,
# skip the version or pick "Remind me later". Nothing runs in between.
import configparser
import json
import os
import subprocess
import sys
import threading
import time
import urllib.request
REPO = "theupriser/steamify-cachyos"
STATE_DIR = os.path.join(os.environ.get("XDG_STATE_HOME") or os.path.expanduser("~/.local/state"),
"steamify")
STATE = os.path.join(STATE_DIR, "notify.state")
DATA = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share")
RUN_APP = os.path.join(DATA, "steamify", "bin", "run-app")
RUN_TERMINAL = os.path.join(DATA, "steamify", "bin", "run-wizard")
RELEASE = f"https://github.com/{REPO}/releases/latest/download"
def version(v):
try:
return tuple(int(p) for p in v.lstrip("v").split("."))
except ValueError:
return None
def state(key):
cp = configparser.ConfigParser(interpolation=None)
cp.read(STATE)
return cp.get("State", key, fallback="")
def set_state(key, value):
# Same ini as lib/state.sh's state_set writes.
subprocess.run(["kwriteconfig6", "--file", STATE, "--group", "State", "--key", key, value], check=False)
def latest():
req = urllib.request.Request(f"https://api.github.com/repos/{REPO}/releases/latest",
headers={"User-Agent": "steamify-notifier", "Accept": "application/vnd.github+json"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["tag_name"].lstrip("v")
def pending(new):
# Newer than the last Steamify that ran here, and not skipped.
seen = version(state("seen") or "0")
return seen is not None and version(new) > seen and new != state("skipped")
def desktop():
# Only on the Plasma desktop: gamescope doesn't show notifications or a
# tray, so an update found in gaming mode waits for the next desktop login.
return subprocess.run(["pgrep", "-u", str(os.getuid()), "-x", "plasmashell"],
stdout=subprocess.DEVNULL).returncode == 0
def main():
if "--delay" in sys.argv:
time.sleep(int(sys.argv[sys.argv.index("--delay") + 1]))
if not desktop():
return 0
try:
new = latest()
except Exception as e: # offline, rate limited: try again next time
print(f"Couldn't check for a new Steamify: {e}", file=sys.stderr)
return 0
if version(new) is None or not pending(new):
return 0
from PySide6.QtCore import QObject, QTimer, Signal
from PySide6.QtGui import QAction, QIcon
from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon
app = QApplication(sys.argv[:1])
app.setApplicationName("Steamify")
app.setDesktopFileName("steamify-ui")
app.setQuitOnLastWindowClosed(False)
icon = QIcon.fromTheme("steamify", QIcon.fromTheme("steam"))
def open_app():
# The way Steamify was last used (the app or the terminal menu), the
# newest release either way; it records its version as seen, which
# ends the reminder. In its own unit: this one's processes are
# stopped when the check exits.
if state("frontend") == "terminal":
cmd = ["konsole", "-e", RUN_TERMINAL] if os.access(RUN_TERMINAL, os.X_OK) else \
["konsole", "-e", "bash", "-c",
f"set -o pipefail; curl -fsSL --max-time 30 {RELEASE}/steamify.sh | bash; read -rp 'Press Enter to close. ' _"]
else:
cmd = [RUN_APP] if os.access(RUN_APP, os.X_OK) else \
["bash", "-c", f"set -o pipefail; curl -fsSL --max-time 30 {RELEASE}/steamify-app.sh | bash"]
# A scope lives as long as anything in it: the start script puts the
# app in the background and exits.
subprocess.Popen(["systemd-run", "--user", "--scope", "--collect", "--quiet", *cmd],
start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
app.quit()
def skip():
set_state("skipped", new)
app.quit()
tray = QSystemTrayIcon(icon)
tray.setToolTip(f"Steamify {new} is available")
menu = QMenu()
for text, fn in ((f"Open Steamify {new}", open_app), ("Skip this version", skip), ("Remind me later", app.quit)):
a = QAction(text, menu)
a.triggered.connect(fn)
menu.addAction(a)
tray.setContextMenu(menu)
tray.activated.connect(lambda reason: open_app() if reason == QSystemTrayIcon.ActivationReason.Trigger else None)
tray.show()
# The notification's buttons: notify-send waits for the click, so it
# runs beside the event loop. Closing it without a click keeps the icon.
class Relay(QObject):
picked = Signal(str)
relay = Relay()
relay.picked.connect(lambda a: open_app() if a == "open" else skip() if a == "skip" else None)
def notify():
r = subprocess.run(["notify-send", "--app-name=Steamify", "--icon=steamify",
"-A", "open=Open Steamify", "-A", "skip=Skip this version",
f"Steamify {new} is available",
"Open Steamify to see what's new and choose what to update."],
capture_output=True, text=True)
relay.picked.emit(r.stdout.strip())
threading.Thread(target=notify, daemon=True).start()
# Opened some other way (the shortcut, the terminal): the reminder is done.
poll = QTimer()
poll.timeout.connect(lambda: None if pending(new) else app.quit())
poll.start(60_000)
return app.exec()
if __name__ == "__main__":
sys.exit(main())
+17
View File
@@ -0,0 +1,17 @@
# Installed by Steamify CachyOS (SteamOS conversion, NVIDIA fix): decides
# again whether the NVIDIA modules go into the initramfs, after DKMS has built
# its modules (71-dkms-install) and before the initramfs is built
# (90-mkinitcpio-install); hooks run in file name order.
[Trigger]
Operation = Install
Operation = Upgrade
Operation = Remove
Type = Path
Target = usr/lib/modules/*/vmlinuz
Target = usr/lib/modules/*/extramodules/nvidia*
Target = usr/lib/modules/*/updates/*
[Action]
Description = Steamify: NVIDIA modules in the initramfs only if every kernel has them
When = PostTransaction
Exec = @SCRIPT@ sync
+36
View File
@@ -0,0 +1,36 @@
#!/bin/bash
# Installed by Steamify CachyOS (SteamOS conversion, NVIDIA fix), run by the
# pacman hook steamify-nvidia-initramfs.hook before the initramfs is built.
# The NVIDIA modules go into the initramfs (early KMS) only while EVERY
# installed kernel has them: mkinitcpio fails on a module a kernel lacks, and
# limine-mkinitcpio then skips that kernel's boot entry, kernel parameters
# included. Decided again at every kernel or driver change, so a new kernel
# without the modules (another kernel, a failed DKMS build, a downgrade) gets
# a working initramfs and the file comes back once it has them.
# steamify-nvidia-initramfs [sync|check]
# sync (default) write or remove the drop-in to match
# check exit status 0 when every installed kernel has nvidia_drm
conf="${NVIDIA_INITRAMFS_CONF:-/etc/mkinitcpio.conf.d/90-steamify-nvidia.conf}"
mods="${NVIDIA_MODULES_DIR:-/usr/lib/modules}"
everywhere() {
local k found=0
for k in "$mods"/*/; do
[[ -f "$k/vmlinuz" || -f "$k/pkgbase" ]] || continue # an installed kernel, not a leftover folder
find "$k" -name 'nvidia-drm.ko*' -print -quit 2>/dev/null | grep -q . || return 1
found=1
done
[[ $found == 1 ]]
}
case "${1:-sync}" in
check) everywhere ;;
sync)
if everywhere; then
[[ -f "$conf" ]] || { mkdir -p "$(dirname "$conf")" &&
echo 'MODULES+=(nvidia nvidia_modeset nvidia_uvm nvidia_drm)' > "$conf"; }
else
rm -f "$conf"
fi ;;
*) echo "usage: $0 [sync|check]" >&2; exit 2 ;;
esac
+14
View File
@@ -0,0 +1,14 @@
# Installed by Steamify CachyOS (SteamOS conversion): runs after
# cachyos-hooks' os-release/cachyos-branding hooks (hooks run in file name
# order).
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = filesystem
Target = cachyos-hooks
[Action]
Description = Steamify: the OS variant and version Steam shows
When = PostTransaction
Exec = @SCRIPT@
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# Installed by Steamify CachyOS (SteamOS conversion): what Steam shows in its
# System settings besides the OS name, which stays CachyOS's: the SteamOS
# release Steamify follows as the OS version (VERSION_ID, e.g. steamos-3.9),
# steam-machine as the codename, and Steamify with its version as the variant
# (VARIANT_ID, which Steam shows). KDE's About this System would show
# VERSION_ID after the name: the conversion sets its UseOSReleaseVersion, so it
# shows os-release's VERSION instead, which CachyOS doesn't have. NAME and
# PRETTY_NAME are never touched: limine-snapper-sync finds the boot entries by
# the OS name. The zz-steamify-os-release hook runs this after cachyos-hooks.
sed -i -e '/^VARIANT=/d' -e '/^VARIANT_ID=/d' -e '/^VERSION_ID=/d' -e '/^VERSION_CODENAME=/d' /etc/os-release
printf '%s\n' 'VARIANT="Steamify @VERSION@"' 'VARIANT_ID=steamify-@VERSION@' 'VERSION_CODENAME=steam-machine' >> /etc/os-release
[ -n "@STEAMOS@" ] && echo 'VERSION_ID=@STEAMOS@' >> /etc/os-release
exit 0
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
SRC="/etc/plasmalogin.conf.d/zz-steamos-autologin.conf"
DEST="/etc/plasmalogin.conf"
[[ -f "$SRC" ]] || exit 0
SESSION=$(grep -oP '^Session=\K.*' "$SRC")
[[ -z "$SESSION" ]] && exit 0
# Only touch Session= inside [Autologin]; other sections may have their own.
if sed -n '/^\[Autologin\]/,/^\[/p' "$DEST" | grep -q '^Session='; then
sed -i "/^\[Autologin\]/,/^\[/ s|^Session=.*|Session=$SESSION|" "$DEST"
elif grep -q '^\[Autologin\]' "$DEST"; then
sed -i "/^\[Autologin\]/a Session=$SESSION" "$DEST"
else
printf '\n[Autologin]\nSession=%s\n' "$SESSION" >> "$DEST"
fi
+16
View File
@@ -0,0 +1,16 @@
# Services
The systemd units Steamify installs, kept out of the shell code so they can
be read and reviewed on their own. `lib/*.sh` reads them with `service_file
<name> [KEY=value...]`, which fills in `@KEY@` placeholders (paths the
scripts define); the single-file build (`.github/tools/bundle.sh`) embeds
them. Small drop-ins that only add a line or two to someone else's unit stay
in the shell code.
| File | Used by | What |
|---|---|---|
| `steamify-boot-desktop.service` | `lib/boot-session.sh` (Boot into: desktop) | Sets the Plasma session before the login manager starts |
| `sync-steamos-session.path`, `.service` | `lib/login-manager.sh` (SteamOS conversion, plasma-login-manager) | The sync bridge: copies Steam's session choice into `plasmalogin.conf` |
| `steamify-steam-autostart.service` | `lib/nvidia.sh` (Gaming on NVIDIA) | Steam at login on the Plasma desktop, normal or in Big Picture (user unit) |
| `ensure-kernel-headers.service` | `lib/steam-machine.sh` (Steam Machine support) | Installs missing kernel headers at boot, so DKMS builds for every kernel |
| `steamify-update-check.service`, `.timer` | `lib/update-notifier.sh` (Update notifications) | The daily and login check for a new release (user units) |
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Install missing kernel headers so DKMS builds leds-valve for every kernel
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=@HEADERS_SCRIPT@
[Install]
WantedBy=multi-user.target
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Steamify CachyOS: boot into the desktop instead of gaming mode
Before=display-manager.service sddm.service plasmalogin.service
[Service]
Type=oneshot
ExecStart=/usr/lib/steamos/steam-set-session plasma.desktop
ExecStart=/bin/sh -c '[ -x @SESSION_SYNC@ ] && exec @SESSION_SYNC@ || true'
[Install]
WantedBy=graphical.target
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=Steam at login (Steamify, NVIDIA PCs)
After=graphical-session.target
PartOf=graphical-session.target
[Service]
Type=simple
# Only in the Plasma desktop session.
ExecCondition=/bin/sh -c '[ "$XDG_CURRENT_DESKTOP" = KDE ]'
ExecStart=/usr/bin/steam @ARGS@
Restart=on-failure
RestartSec=5
[Install]
WantedBy=graphical-session.target
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=Steamify: check for a new release
After=plasma-workspace.target
# The tray icon goes with a logout.
PartOf=graphical-session.target
[Service]
Type=simple
# The short wait at login lets the tray come up first (in the script: an
# ExecStartPre sleep runs into the start timeout).
# Quoted: the path is in the home folder, which may contain a space.
ExecStart="@NOTIFY_SCRIPT@" --delay 60
[Install]
WantedBy=plasma-workspace.target
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Steamify: check for a new release daily
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
+8
View File
@@ -0,0 +1,8 @@
[Unit]
Description=Watch for steamos session changes
[Path]
PathModified=/etc/plasmalogin.conf.d/zz-steamos-autologin.conf
[Install]
WantedBy=multi-user.target
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=Sync steamos session selection into plasmalogin.conf
# Session switches can come in bursts (switch + autologin reset); never
# let systemd's start rate limit silently stop the bridge.
StartLimitIntervalSec=0
[Service]
Type=oneshot
ExecStart=/usr/local/bin/sync-steamos-session.sh
+88 -6
View File
@@ -15,20 +15,38 @@
set -uo pipefail set -uo pipefail
# Release version, see CHANGELOG.md. # Release version, see CHANGELOG.md.
VERSION=1.1.2 VERSION=2.11.0
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for lib in common state packages login-manager single-user steam-desktop steam-machine cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu; do for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh nvidia controllers cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut steam-game update-notifier first-login menu backend; do
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$SCRIPT_DIR/lib/$lib.sh" source "$SCRIPT_DIR/lib/$lib.sh"
done done
require_root_helper require_root_helper
echo -e "${c_bold}Steamify CachyOS${c_reset} v$VERSION" BACKEND=false
echo "Turn the SteamOS-style parts on or off. The menu shows what is on now;" [[ "${1:-}" == --backend ]] && BACKEND=true
echo "anything you turn off is put back the way it was." # --defaults [--options <id>,...] [--boot gamescope|desktop]: apply what the
# menu would preselect (or exactly the listed items), without the menu or any
# prompt (the Steam Machine ISO's first login runs this; sudo must not ask).
DEFAULTS=false
[[ "${1:-}" == --defaults ]] && DEFAULTS=true
# --defaults --list: what --defaults can set up here, as JSON (an installer
# page is built from it); nothing else on stdout.
LIST=false
[[ "$DEFAULTS" == true && "${2:-}" == --list ]] && LIST=true
# --boot gamescope|desktop: only change where an installed conversion
# starts, everything else stays as it is (for scripts; no menu).
BOOT_ONLY=false
[[ "${1:-}" == --boot ]] && BOOT_ONLY=true
if [[ "$BACKEND" == false && "$LIST" == false ]]; then
echo -e "${c_bold}Steamify CachyOS${c_reset} v$VERSION"
echo "Turn the SteamOS-style parts on or off. The menu shows what is on now;"
echo "anything you turn off is put back the way it was."
fi
if ! command -v pacman >/dev/null 2>&1; then if ! command -v pacman >/dev/null 2>&1; then
err "This doesn't look like an Arch/CachyOS system (no pacman found). Aborting." err "This doesn't look like an Arch/CachyOS system (no pacman found). Aborting."
@@ -38,6 +56,8 @@ fi
# Everything per-user (autologin user, Steam, theme, shortcut) is for the # Everything per-user (autologin user, Steam, theme, shortcut) is for the
# user running the script. # user running the script.
TARGET_USER="$(id -un)" TARGET_USER="$(id -un)"
migrate_layout
notify_seen
restart_needed() { [[ -n "${BIOS_NEEDS_RESTART:-}" || "$RESTART_FOR_LOGIN" == true ]]; } restart_needed() { [[ -n "${BIOS_NEEDS_RESTART:-}" || "$RESTART_FOR_LOGIN" == true ]]; }
@@ -98,6 +118,64 @@ quit_prompt() {
return 0 return 0
} }
# The graphical app (steamify-ui) drives the same components through
# lib/backend.sh instead of the menu.
if [[ "$BACKEND" == true ]]; then
RESTART_FOR_LOGIN=false
shift
backend_main "$@"
exit $?
fi
if [[ "${1:-}" == --first-login ]]; then
first_login_run
exit 0
fi
if [[ "$LIST" == true ]]; then
defaults_list
exit 0
fi
if [[ "$DEFAULTS" == true ]]; then
RESTART_FOR_LOGIN=false
REAPPLY=false
sudo -n true 2>/dev/null || { err "--defaults needs sudo without a password."; exit 1; }
detect_components
defaults_options "${@:2}" || exit 1
plan_changes
apply_changes
# Run by the installer: the rest waits for the first desktop login.
user_session || first_login_schedule
[[ ${#FAILED[@]} -eq 0 ]] || { warn "These had problems (see above): ${FAILED[*]}"; exit 1; }
ok "Done; the changes take effect after a restart."
exit 0
fi
if [[ "$BOOT_ONLY" == true ]]; then
RESTART_FOR_LOGIN=false
REAPPLY=false
case "${2:-}" in
gamescope|desktop) ;;
*) err "--boot takes gamescope or desktop"; exit 1 ;;
esac
detect_components
[[ "${CURRENT[gaming]}" == 1 ]] || { err "--boot needs the SteamOS conversion; turn it on first."; exit 1; }
# What is on stays on, and nothing else changes: not the updates or
# removals a normal run would pick.
for c in "${COMPONENTS[@]}"; do WANTED[$c]=${CURRENT[$c]:-0}; done
WANTED[boot]=0; [[ "$2" == desktop ]] && WANTED[boot]=1
plan_changes
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
ok "Already starting in $2."
exit 0
fi
apply_changes
[[ ${#FAILED[@]} -eq 0 ]] || { warn "These had problems (see above): ${FAILED[*]}"; exit 1; }
ok "Starts in $2 from the next boot on."
exit 0
fi
# Menu loop: after each run the menu comes back with the new state, until # Menu loop: after each run the menu comes back with the new state, until
# the user quits; the restart question comes then, once, for everything. # the user quits; the restart question comes then, once, for everything.
RESTART_FOR_LOGIN=false RESTART_FOR_LOGIN=false
@@ -111,6 +189,7 @@ while true; do
plan_changes plan_changes
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
feature_record_unticked
ok "Everything is already the way you want it." ok "Everything is already the way you want it."
read -rp "Press Enter to go back to the menu... " _ || break read -rp "Press Enter to go back to the menu... " _ || break
continue continue
@@ -125,7 +204,10 @@ while true; do
for c in "${TO_ENABLE[@]}"; do for c in "${TO_ENABLE[@]}"; do
if [[ "$c" == boot ]]; then echo " - boot into: desktop (from the next boot)" if [[ "$c" == boot ]]; then echo " - boot into: desktop (from the next boot)"
elif is_action "$c"; then echo " - run: ${LABEL[$c]%%:*} at your own risk (checks, then asks twice more)" elif is_action "$c"; then echo " - run: ${LABEL[$c]%%:*} at your own risk (checks, then asks twice more)"
elif [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}"; else echo " - turn on: ${LABEL[$c]}"; fi elif feature_outdated "$c"; then echo " - update: ${LABEL[$c]} (changed in this version)"
elif [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}"
elif feature_new "$c"; then echo " - turn on: ${LABEL[$c]} (new in this version)"
else echo " - turn on: ${LABEL[$c]}"; fi
done done
ask_yn "Go ahead?" y || { info "Nothing changed."; continue; } ask_yn "Go ahead?" y || { info "Nothing changed."; continue; }
+292
View File
@@ -0,0 +1,292 @@
// State and logic of the app: menu -> review -> password -> applying -> done,
// and the BIOS update (check -> warning 1 -> warning 2 -> flash). The
// screens only show this and call its functions.
import QtQuick
Item {
id: app
required property var backend
required property var gamepad
property string screen: "menu" // menu review password applying done bios1 bios2
property double remoteAt: 0
property var want: ({})
property string boot: "gamescope"
property int sel: 0 // row index; rows.length = the Apply button
property bool reapply: false
property var plan: [] // [{id, action, isNew}]
property var steps: ({}) // id -> "wait"|"run"|"ok"|"fail"
property var failed: []
property bool restartNeeded: false
// Review & apply only when something would change; "Re-apply what's on"
// is for when nothing did.
readonly property bool canApply: computePlan().length > 0
onCanApplyChanged: if (!canApply && sel === rows.length) sel = Math.max(0, rows.length - 1)
property string runError: ""
property bool wrongPassword: false
property bool typing: false // a text field has the keys
property int doneCount: 0
property string pending: "apply" // what the password is for: apply | bios
property string sessionPassword: "" // kept between the BIOS steps only
property var biosInfo: ({})
property bool biosRun: false
property bool biosChecking: false // the invisible check before the warnings
property int biosFocus: 0 // warning 1: 0 = Cancel, 1 = continue
property real holdProgress: 0 // warning 2: hold A/OK for 5 s
property bool holding: false
readonly property alias log: logModel
readonly property var status: backend.status || ({})
readonly property var items: status.items || []
// Hidden: Big Picture excludes a silent start.
function hidden(i) { return i.id === "nvsilent" && want.bigpicture; }
readonly property var rows: items.filter(function (i) { return (!i.parent || want[i.parent]) && !hidden(i); })
readonly property var bios: status.bios || null
signal passwordRequested()
signal passwordRejected()
signal passwordSubmitRequested() // A/OK on the password screen
ListModel { id: logModel }
function nowOn(id) {
for (var i = 0; i < items.length; i++) if (items[i].id === id) return items[i].on;
return false;
}
function label(item) { return Texts.label(item.id, item.label); }
function biosHint() {
if (!bios) return "";
if (!bios.newest) return "Now " + bios.current + ", newest unknown (offline?)";
if (bios.current === bios.newest) return bios.current + " is up to date";
return "Now " + bios.current + ", newest " + bios.newest + " · at your own risk";
}
function syncFromStatus() {
reapply = false;
var w = {};
for (var i = 0; i < items.length; i++) w[items[i].id] = items[i].kind === "action" ? false : items[i].wanted;
want = w;
boot = nowOn("boot") ? "desktop" : "gamescope";
if (sel > rows.length) sel = 0;
}
Connections { target: backend; function onStatusChanged() { if (app.screen === "menu" || app.screen === "done") app.syncFromStatus(); } }
// --- Menu ---
function toggle(id) {
var cur = items.find(function (i) { return i.id === id; });
if (cur && cur.kind === "toggle" && cur.selectable === false) return;
var w = Object.assign({}, want);
w[id] = !w[id];
// The same rules as the terminal menu (toggle_component).
var hasGaming = items.some(function (i) { return i.id === "gaming"; });
if (id === "gaming" && !w.gaming) { w.single = false; w.silent = false; w.glyphs = false; }
if (id === "single" && w.single && hasGaming) w.gaming = true;
if (id === "nvidia") w.bigpicture = w.nvidia;
if (id === "bigpicture" && w.bigpicture) { w.nvidia = true; w.nvsilent = false; }
if (id === "nvsilent" && w.nvsilent) { w.nvidia = true; w.bigpicture = false; }
if (id === "nvidia" && !w.nvidia) w.nvsilent = false;
if (id === "silent" && w.silent) w.gaming = true;
if (id === "glyphs" && w.glyphs) w.gaming = true;
if (id === "machine") w.poweroff = w.machine;
if (id === "poweroff" && w.poweroff) w.machine = true;
if (id === "machine" && !w.machine) w.kpin = false;
if (id === "kpin" && w.kpin) w.machine = true;
if (id === "launcher" && items.some(function (i) { return i.id === "steamgame"; })) w.steamgame = w.launcher;
if (id === "steamgame" && w.steamgame) w.launcher = true;
want = w;
}
function computePlan() {
var p = [];
for (var i = 0; i < items.length; i++) {
var it = items[i];
if (it.kind !== "toggle") continue;
if (hidden(it)) { if (it.on) p.push({ id: it.id, action: "off" }); continue; }
if (it.parent && !want[it.parent]) { if (it.on) p.push({ id: it.id, action: "off" }); continue; }
if (want[it.id] && !it.on) p.push({ id: it.id, action: "on", isNew: !!it["new"] });
else if (!want[it.id] && it.on) p.push({ id: it.id, action: "off" });
else if (want[it.id] && (reapply || it.update)) p.push({ id: it.id, action: reapply ? "again" : "update" });
}
var bootNow = nowOn("boot") ? "desktop" : "gamescope";
if (want.gaming && boot !== bootNow) p.push({ id: "boot", action: boot === "desktop" ? "desktop" : "gaming" });
return p;
}
function goReview(again) {
if (!again && !canApply) return;
reapply = again; plan = computePlan(); screen = "review";
}
function wantedIds() {
var ids = [];
for (var i = 0; i < items.length; i++)
if (items[i].kind === "toggle" && want[items[i].id] && !hidden(items[i]) && (!items[i].parent || want[items[i].parent])) ids.push(items[i].id);
return ids;
}
// --- Password and apply ---
function resetRun() { steps = {}; failed = []; restartNeeded = false; runError = ""; doneCount = 0; logModel.clear(); }
function startApply(password) {
resetRun();
var s = {};
for (var i = 0; i < plan.length; i++) s[plan[i].id] = "wait";
steps = s;
screen = "applying";
backend.apply(wantedIds(), want.gaming ? boot : "", reapply, password || "");
}
function askPassword(forWhat) {
pending = forWhat;
if (!backend.needsPassword()) return false;
wrongPassword = false; screen = "password"; passwordRequested();
return true;
}
function onApplyPressed() {
if (plan.length === 0) return;
if (!askPassword("apply")) startApply("");
}
function submitPassword(p) {
if (!backend.checkPassword(p)) { wrongPassword = true; passwordRejected(); return; }
if (pending === "bios") { sessionPassword = p; biosCheck(); }
else startApply(p);
}
function passwordBack() { screen = pending === "bios" ? "menu" : "review"; }
// --- BIOS ---
function startBios() {
if (!bios || !bios.selectable) return;
biosInfo = {}; sessionPassword = "";
if (!askPassword("bios")) biosCheck();
}
function biosCheck() {
// Invisible: stay on the menu (the BIOS row says "Checking…"),
// then the first warning.
failed = []; runError = ""; logModel.clear();
plan = [{ id: "bios", action: "check" }];
biosRun = true; biosChecking = true; screen = "menu";
backend.biosPrepare(sessionPassword);
}
function biosCancel() { sessionPassword = ""; holdProgress = 0; holding = false; biosRun = false; syncFromStatus(); screen = "menu"; }
function biosContinue() { holdProgress = 0; screen = "bios2"; }
function biosFlash() {
holdProgress = 0; holding = false;
resetRun();
plan = [{ id: "bios", action: "flash" }];
steps = { bios: "wait" };
biosRun = true; screen = "applying";
backend.biosFlash(sessionPassword);
}
Timer {
interval: 50; repeat: true; running: app.screen === "bios2" && app.holding
onTriggered: { app.holdProgress = Math.min(1, app.holdProgress + 0.01); if (app.holdProgress >= 1) app.biosFlash(); }
}
function backToMenu() { biosRun = false; syncFromStatus(); screen = "menu"; }
// --- Backend events ---
function errorText(e) {
return e === "wrong-password" ? "The password didn't work." : (e === "start" ? "Steamify couldn't start the changes." : (e ? "sudo isn't available." : ""));
}
Connections {
target: app.backend
function onEvent(ev) {
var s = Object.assign({}, app.steps);
if (ev.event === "start") { s[ev.id] = "run"; app.steps = s; }
else if (ev.event === "done") { s[ev.id] = ev.ok ? "ok" : "fail"; app.steps = s; app.doneCount++; }
else if (ev.event === "log") { logModel.append({ line: ev.line }); if (logModel.count > 400) logModel.remove(0); }
else if (ev.event === "bios-ready") { app.biosChecking = false; app.biosInfo = ev; app.biosFocus = 0; app.holdProgress = 0; app.screen = "bios1"; }
else if (ev.event === "finished" && app.biosChecking) {
// The check found nothing to do or a problem: say so.
app.biosChecking = false; app.sessionPassword = "";
app.failed = ev.failed || []; app.restartNeeded = false;
app.runError = ev.nothing && !ev.error ? "The BIOS is already the newest version." : app.errorText(ev.error);
app.screen = "done";
}
else if (ev.event === "finished") {
// A successful BIOS check ends with bios-ready instead, keeping
// the password for the flash.
app.sessionPassword = "";
app.failed = ev.failed || []; app.restartNeeded = !!ev.restart;
app.runError = app.errorText(ev.error);
app.screen = "done";
}
}
function onRemotePressed() { app.remoteAt = Date.now(); Input.type = "remote"; }
}
// --- Input: one set of actions for controller, keyboard and remote ---
function act(a) {
if (screen === "menu") {
if (biosChecking) return;
if (a === "up") sel = Math.max(0, sel - 1);
// The greyed-out Apply button can't be selected.
else if (a === "down") sel = Math.min(canApply ? rows.length : rows.length - 1, sel + 1);
else if (a === "accept") {
if (sel === rows.length) { goReview(false); return; }
var r = rows[sel];
if (r.kind === "action" && r.id === "bios") startBios();
else if (r.kind === "choice") boot = boot === "gamescope" ? "desktop" : "gamescope";
else if (r.kind === "toggle") toggle(r.id);
}
else if ((a === "left" || a === "right") && sel < rows.length && rows[sel].kind === "choice")
boot = a === "left" ? "gamescope" : "desktop";
else if (a === "apply") goReview(false);
else if (a === "reapply") goReview(true);
// B/Esc/Back don't quit: Steam's desktop layout sends Esc for a
// Steam Controller's B, which should only ever go back.
else if (a === "quit") Qt.quit();
} else if (screen === "review") {
if (a === "accept" || a === "apply") onApplyPressed();
else if (a === "back") { reapply = false; screen = "menu"; }
} else if (screen === "password") {
if (a === "accept" || a === "apply") passwordSubmitRequested();
else if (a === "back") passwordBack();
} else if (screen === "bios1") {
if (a === "left") biosFocus = 0;
else if (a === "right") biosFocus = 1;
else if (a === "accept") { if (biosFocus === 1) biosContinue(); else biosCancel(); }
else if (a === "back") biosCancel();
} else if (screen === "bios2") {
if (a === "back") biosCancel();
else if (a === "hold") holding = true;
else if (a === "release") { holding = false; if (holdProgress < 1) holdProgress = 0; }
} else if (screen === "done") {
if (a === "accept" && restartNeeded) backend.restart();
else if (a === "back" || a === "accept") backToMenu();
}
}
Connections {
target: app.gamepad
function onButton(b) {
if (b === "a_up") { if (app.screen === "bios2") app.act("release"); return; }
if (b.slice(-3) === "_up") return;
Input.type = "controller";
if (b === "a" && app.screen === "bios2") { app.act("hold"); return; }
var map = { a: "accept", b: "back", x: "apply", y: "reapply", start: "apply", up: "up", down: "down", left: "left", right: "right" };
if (map[b]) app.act(map[b]);
}
}
function keyPressed(e) {
// A text field handles its own keys, except Escape and Enter.
if (typing && e.key !== Qt.Key_Escape && e.key !== Qt.Key_Return && e.key !== Qt.Key_Enter) return;
var fromRemote = Date.now() - remoteAt < 400;
if (screen === "bios2" && Input.type !== "keyboard" && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) {
if (!e.isAutoRepeat) act("hold"); e.accepted = true; return;
}
if (!fromRemote) Input.type = Input.fromSteam ? "steam" : "keyboard";
var k = e.key;
if (k === Qt.Key_Up) act("up");
else if (k === Qt.Key_Down) act("down");
else if (k === Qt.Key_Left) act("left");
else if (k === Qt.Key_Right) act("right");
else if (k === Qt.Key_Space) act("accept");
// Enter selects, like A on a controller: Steam's desktop layout sends
// Enter for a Steam Controller's A (and Space for Y). Ctrl+Enter
// goes straight to Review & apply.
else if ((k === Qt.Key_Return || k === Qt.Key_Enter) && (e.modifiers & Qt.ControlModifier)) act("apply");
else if (k === Qt.Key_Return || k === Qt.Key_Enter) act("accept");
else if (k === Qt.Key_Escape || k === Qt.Key_Back || k === Qt.Key_Backspace) { if (!e.isAutoRepeat) act("back"); }
else if (k === Qt.Key_R) act("reapply");
else if (k === Qt.Key_Q && (e.modifiers & Qt.ControlModifier)) act("quit");
else return;
e.accepted = true;
}
function keyReleased(e) {
if (screen === "bios2" && !e.isAutoRepeat && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) act("release");
}
}
+66
View File
@@ -0,0 +1,66 @@
// Progress: every step of the plan on the left, the log on the right.
import QtQuick
import QtQuick.Controls.Basic
Item {
required property AppState app
readonly property bool checking: app.biosRun && app.plan.length > 0 && app.plan[0].action === "check"
Column {
x: 40; y: 32; width: 560; spacing: 18
Text { text: app.biosRun ? (checking ? "Checking the BIOS update" : "Installing the BIOS update") : "Applying your changes"
color: Theme.text; font.family: Theme.display; font.pixelSize: 34; font.weight: Font.DemiBold }
Rectangle { width: 560; height: 10; radius: 5; color: Theme.line
Rectangle { height: 10; radius: 5; color: Theme.accent; width: app.plan.length ? parent.width * Math.min(1, (app.doneCount + 0.3) / app.plan.length) : 0
Behavior on width { NumberAnimation { duration: 300 } } } }
Row { width: 560
Text { text: "Step " + Math.min(app.plan.length, app.doneCount + 1) + " of " + app.plan.length; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14; width: 280 }
Text { text: "Don't turn off the PC"; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14; width: 280; horizontalAlignment: Text.AlignRight } }
ListView {
width: 560; clip: true; spacing: 8
height: parent.parent.height - 32 - 156 - 32
model: app.plan
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
// Follow the progress: the running step stays in view.
readonly property int running: app.plan.findIndex(function (p) { return app.steps[p.id] === "run"; })
onRunningChanged: if (running >= 0) positionViewAtIndex(running, ListView.Contain)
delegate: Rectangle {
id: step
required property var modelData
readonly property string st: app.steps[modelData.id] || "wait"
width: 560; height: 54; radius: 12; color: st === "run" ? Theme.cardSel : Theme.card
border.width: st === "run" ? 2 : 0; border.color: Theme.accent
Row { anchors.fill: parent; anchors.leftMargin: 18; anchors.rightMargin: 18; spacing: 14
Rectangle { width: 22; height: 22; radius: 11; anchors.verticalCenter: parent.verticalCenter
color: step.st === "ok" ? Theme.goodBg : (step.st === "fail" ? Theme.badBg : "transparent")
border.width: step.st === "wait" || step.st === "run" ? 2 : 0; border.color: step.st === "run" ? Theme.accent : "#343f50"
Text { anchors.centerIn: parent; text: step.st === "ok" ? "✓" : (step.st === "fail" ? "!" : ""); color: step.st === "ok" ? Theme.good : Theme.bad
font.pixelSize: 13; font.weight: Font.Bold }
// Only this dot turns while the step runs: ✓ and ! never rotate
// (an animator leaves its item at whatever angle it stopped).
Item { anchors.fill: parent; visible: step.st === "run"
Rectangle { width: 6; height: 6; radius: 3; color: Theme.accent; x: parent.width / 2 - 3; y: -3 }
RotationAnimator on rotation { running: step.st === "run"; from: 0; to: 360; duration: 1000; loops: Animation.Infinite } } }
Row { width: 380; anchors.verticalCenter: parent.verticalCenter; spacing: 10
// Boot into's label is its prefix already: "Boot into desktop", not "Boot into Boot into".
Text { text: step.modelData.id === "boot" ? (Texts.actions[step.modelData.action] || [])[3] + Texts.actions[step.modelData.action][0].toLowerCase()
: (Texts.actions[step.modelData.action] || [])[3] + Texts.label(step.modelData.id)
color: step.st === "wait" ? Theme.faint : Theme.textHi; font.family: Theme.body; font.pixelSize: 16; font.weight: Font.DemiBold
anchors.verticalCenter: parent.verticalCenter; elide: Text.ElideRight
width: Math.min(implicitWidth, parent.width - (badge.visible ? badge.width + parent.spacing : 0)) }
Badge { id: badge; update: step.modelData.action === "update"; isNew: !!step.modelData.isNew; anchors.verticalCenter: parent.verticalCenter } }
Text { text: ({ wait: "Waiting", run: "Working…", ok: "Done", fail: "Problem" })[step.st]
color: step.st === "ok" ? Theme.good : (step.st === "fail" ? Theme.bad : Theme.label); font.family: Theme.body; font.pixelSize: 13
anchors.verticalCenter: parent.verticalCenter }
}
}
}
}
Column {
x: 632; y: 32; width: parent.width - 672; height: parent.height - 64; spacing: 10
Text { text: "DETAILS"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
Rectangle { width: parent.width; height: parent.height - 30; radius: 16; color: "#0b0e13"; border.width: 1; border.color: "#1d2531"
ListView { id: logView; anchors.fill: parent; anchors.margins: 18; clip: true; model: app.log
onCountChanged: positionViewAtEnd()
delegate: Text { required property string line; width: logView.width; text: line; color: "#aab5c4"; font.family: Theme.mono; font.pixelSize: 13; wrapMode: Text.WrapAnywhere } } }
}
}
+13
View File
@@ -0,0 +1,13 @@
// "Update" (a newer version of it will be applied) or "New" (a default
// option added since the last run), after an item's name.
import QtQuick
Chip {
property bool update: false
property bool isNew: false
visible: update || isNew
height: 20
text: update ? "Update" : "New"
fg: update ? Theme.warn : Theme.good
bgc: update ? Theme.warnBg : Theme.goodBg
}
+7
View File
@@ -0,0 +1,7 @@
// The bar at the bottom of a screen, for its buttons.
import QtQuick
Rectangle {
anchors.bottom: parent.bottom; width: parent.width; height: 64; color: Theme.bar
Rectangle { width: parent.width; height: 1; color: Theme.line }
}
+36
View File
@@ -0,0 +1,36 @@
// BIOS update, warning 2 (last chance): type UPDATE on a keyboard, or hold
// A/OK for 5 seconds on a controller or remote.
import QtQuick
import QtQuick.Controls.Basic
Item {
id: screen
required property AppState app
onVisibleChanged: if (visible) { typed.text = ""; if (Input.type === "keyboard") typed.forceActiveFocus(); }
Binding { target: screen.app; property: "typing"; value: typed.activeFocus; when: screen.visible }
Rectangle {
anchors.centerIn: parent; width: 760; height: col.implicitHeight + 64; radius: 20; color: Theme.dangerBg; border.width: 2; border.color: Theme.dangerLine
Column { id: col; anchors.fill: parent; anchors.margins: 32; spacing: 18
Text { text: "Last chance: this flashes BIOS " + (app.biosInfo.newest || ""); color: "#ffd9d3"; font.family: Theme.display; font.pixelSize: 32; font.weight: Font.DemiBold }
Text { width: parent.width; wrapMode: Text.WordWrap; color: Theme.soft; font.family: Theme.body; font.pixelSize: 16; lineHeight: 1.3
text: "After this, keep the power on until the machine has fully started again. Don't touch it, even if the screen is black." }
Column { visible: Input.type === "keyboard"; width: parent.width; spacing: 8
Text { text: "Type UPDATE (in capitals) and press Enter to flash the BIOS:"; color: Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold }
TextField { id: typed; width: 320; height: 50; color: Theme.textHi; font.family: Theme.mono; font.pixelSize: 20; leftPadding: 14
placeholderText: "UPDATE"; placeholderTextColor: "#6b5a5a"
background: Rectangle { radius: 10; color: Theme.bg; border.width: 2; border.color: typed.text === "UPDATE" ? Theme.bad : "#5a3a3a" }
Keys.onReturnPressed: if (text === "UPDATE") app.biosFlash()
Keys.onEnterPressed: if (text === "UPDATE") app.biosFlash()
Keys.onEscapePressed: app.biosCancel() } }
Column { visible: Input.type !== "keyboard"; width: parent.width; spacing: 10
Text { text: "Hold " + Input.g.ok + " for 5 seconds to flash the BIOS. Let go to stop."; color: Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold }
Rectangle { width: parent.width; height: 14; radius: 7; color: "#3a2224"
Rectangle { height: 14; radius: 7; color: Theme.bad; width: parent.width * app.holdProgress } } }
Row { spacing: 12
Btn { k: Input.g.back; text: "Cancel"; onClicked: app.biosCancel() }
Text { visible: app.bios && app.bios.dryRun; text: "Dry run: nothing will be flashed."; color: Theme.warn; font.family: Theme.body; font.pixelSize: 14
anchors.verticalCenter: parent.verticalCenter } }
}
}
}
+37
View File
@@ -0,0 +1,37 @@
// BIOS update, warning 1: the risks and what was checked; Cancel is
// selected first.
import QtQuick
Item {
required property AppState app
Rectangle {
x: 40; y: 28; width: parent.width - 80; height: parent.height - 28 - 64 - 24; radius: 16; color: Theme.dangerBg; border.width: 2; border.color: Theme.dangerLine
Row { anchors.fill: parent; anchors.margins: 32; spacing: 40
Column { width: 640; spacing: 16
Row { spacing: 12
Rectangle { width: 44; height: 44; radius: 12; color: "#3a1c1f"
Text { anchors.centerIn: parent; text: "!"; color: Theme.bad; font.pixelSize: 26; font.weight: Font.Bold } }
Text { text: "BIOS update – entirely at your own risk"; color: "#ffd9d3"; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold
anchors.verticalCenter: parent.verticalCenter } }
RiskList { width: 640 }
}
Rectangle { width: 440; height: 250; radius: 14; color: Theme.card
Column { anchors.fill: parent; anchors.margins: 22; spacing: 12
Repeater { model: [["Current BIOS", app.biosInfo.current || ""], ["New BIOS", app.biosInfo.newest || ""], ["Checksum", "OK (Valve's package)"],
["Compatible", app.biosInfo.compatible === "yes" ? "Yes (checked by fwupd)" : "Not checked (dry run)"]]
Row { required property var modelData; width: 396
Text { text: parent.modelData[0]; color: Theme.faint; font.family: Theme.body; font.pixelSize: 15; width: 150 }
Text { text: parent.modelData[1]; color: parent.modelData[1].indexOf("Not") === 0 ? Theme.warn : Theme.text; font.family: Theme.mono; font.pixelSize: 15
width: 246; horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } } }
} }
}
}
Bar {
Text { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; text: "Do you understand the risks and want to continue?"
color: Theme.soft; font.family: Theme.body; font.pixelSize: 15 }
Row { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 12
Btn { k: app.biosFocus === 0 ? Input.g.ok : "◀"; text: "Cancel"; focusRing: app.biosFocus === 0; onClicked: app.biosCancel() }
Btn { k: app.biosFocus === 1 ? Input.g.ok : "▶"; text: "I understand, continue"; focusRing: app.biosFocus === 1; color: Theme.danger
onClicked: app.biosContinue() } }
}
}
+22
View File
@@ -0,0 +1,22 @@
// A button with the input's button name in front.
import QtQuick
Rectangle {
id: btn
property string k
property string text
property bool primary: false
property bool focusRing: false
signal clicked
height: 48; radius: 12; width: row.implicitWidth + 36
color: primary ? Theme.accent : Theme.button
border.width: focusRing && enabled ? 2 : 0; border.color: Theme.textHi
// Greyed out while there's nothing to do.
opacity: enabled ? 1 : 0.4
Row { id: row; anchors.centerIn: parent; spacing: 10
Glyph { k: btn.k; visible: btn.k !== ""; dark: btn.primary; anchors.verticalCenter: parent.verticalCenter }
Text { text: btn.text; color: btn.primary ? Theme.ink : Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold
anchors.verticalCenter: parent.verticalCenter }
}
MouseArea { anchors.fill: parent; enabled: btn.enabled; cursorShape: Qt.PointingHandCursor; onClicked: btn.clicked() }
}
+12
View File
@@ -0,0 +1,12 @@
// A small label in capitals.
import QtQuick
Rectangle {
id: chip
property string text
property color fg: Theme.good
property color bgc: Theme.goodBg
height: 24; radius: 6; width: ct.implicitWidth + 16; color: bgc
Text { id: ct; anchors.centerIn: parent; text: chip.text.toUpperCase(); color: chip.fg
font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.6 }
}
+45
View File
@@ -0,0 +1,45 @@
// Right of the menu: what the selected item does, or a nudge to review
// when the Apply button is selected.
import QtQuick
import QtQuick.Controls.Basic
Rectangle {
id: detail
required property AppState app
readonly property var it: app.sel < app.rows.length ? app.rows[app.sel] : null
readonly property var tx: it ? Texts.of(it) : ({})
radius: 16; color: Theme.card
// Long texts scroll instead of running out of the card; each item
// starts at the top.
onItChanged: flick.contentY = 0
Flickable {
id: flick
anchors.fill: parent; anchors.margins: 24; anchors.rightMargin: 12
visible: detail.it !== null; clip: true
contentHeight: col.implicitHeight; boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
Column {
id: col; width: flick.width - 12; spacing: 14
Row { spacing: 10
Chip { readonly property var it: detail.it
text: !it ? "" : it.kind === "choice" ? (app.boot === "desktop" ? "Desktop" : "Gaming") : it.kind === "action" ? "Opt-in" : (it.on ? "On" : "Off")
fg: it && it.on ? Theme.good : Theme.label; bgc: it && it.on ? Theme.goodBg : Theme.line }
Chip { visible: !!detail.tx.experimental; text: "Experimental"; fg: Theme.warn; bgc: Theme.warnBg }
}
Text { text: detail.it ? app.label(detail.it) : ""; color: Theme.text; font.family: Theme.display; font.pixelSize: 26; font.weight: Font.DemiBold
wrapMode: Text.WordWrap; width: parent.width }
Text { text: detail.tx.body || ""; color: Theme.soft; font.family: Theme.body; font.pixelSize: 15; lineHeight: 1.4; wrapMode: Text.WordWrap; width: parent.width }
Text { text: "WHAT IT CHANGES"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8; topPadding: 4 }
Repeater { model: detail.tx.changes || []
Text { required property string modelData; text: "• " + modelData; color: Theme.soft; font.family: Theme.body; font.pixelSize: 14
wrapMode: Text.WordWrap; width: col.width } }
}
}
Column {
anchors.centerIn: parent; spacing: 8; visible: detail.it === null
Text { text: app.computePlan().length === 0 ? "Everything is the way you want it" : "Review what will change"; color: Theme.text
font.family: Theme.display; font.pixelSize: 24; anchors.horizontalCenter: parent.horizontalCenter }
Text { text: "then apply it"; color: Theme.mute; font.family: Theme.body; font.pixelSize: 15; anchors.horizontalCenter: parent.horizontalCenter }
}
}
+38
View File
@@ -0,0 +1,38 @@
// The result of a run: all done, a problem, or nothing changed. With a
// problem, the run's output is shown below it (a BIOS check has no progress
// screen, so this is the only place to read why it stopped).
import QtQuick
import QtQuick.Controls.Basic
Item {
required property AppState app
readonly property bool problem: app.failed.length > 0 || app.runError !== ""
readonly property string message: {
if (app.runError) return app.runError;
if (!app.biosRun) return app.plan.length + (app.plan.length === 1 ? " change" : " changes") + " applied." + (app.restartNeeded ? " Some take effect after a restart." : "");
if (app.failed.length) return "The BIOS was not changed.";
return (app.bios && app.bios.dryRun ? "Dry run: nothing was flashed. " : "BIOS " + (app.biosInfo.newest || "") + " is staged and is written during the restart. ")
+ "Keep the power on and don't touch the machine until it has fully started again, even if the screen stays black.";
}
Column {
anchors.centerIn: parent; width: 760; spacing: 20
Rectangle { width: 72; height: 72; radius: 36; anchors.horizontalCenter: parent.horizontalCenter; color: problem ? Theme.warnBg : Theme.goodBg
Text { anchors.centerIn: parent; text: problem ? "!" : "✓"; color: problem ? Theme.warn : Theme.good; font.pixelSize: 36; font.weight: Font.Bold } }
Text { text: app.runError ? "Nothing changed" : (app.biosRun && app.failed.length ? "The BIOS update stopped" : (app.failed.length ? "Done, with a problem" : "All done"))
color: Theme.text; font.family: Theme.display; font.pixelSize: 40; font.weight: Font.DemiBold; anchors.horizontalCenter: parent.horizontalCenter }
Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap; color: Theme.soft; font.family: Theme.body; font.pixelSize: 17; text: message }
Rectangle { visible: app.failed.length > 0; anchors.horizontalCenter: parent.horizontalCenter; width: ft.implicitWidth + 32; height: 44; radius: 12; color: Theme.warnBg
Text { id: ft; anchors.centerIn: parent; color: "#f2c27a"; font.family: Theme.body; font.pixelSize: 14
text: "Had a problem: " + app.failed.map(function (id) { return Texts.label(id); }).join(", ") + (app.log.count ? ". Details below." : ".") } }
Rectangle { visible: app.failed.length > 0 && app.log.count > 0; width: parent.width; height: 190; radius: 14; color: Theme.card
ListView { id: details; anchors.fill: parent; anchors.margins: 16; clip: true; model: app.log
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
// The end is where it stopped.
onCountChanged: positionViewAtEnd()
onVisibleChanged: if (visible) positionViewAtEnd()
delegate: Text { required property string line; width: details.width - 12; text: line; color: "#aab5c4"; font.family: Theme.mono; font.pixelSize: 13; wrapMode: Text.WrapAnywhere } } }
Row { anchors.horizontalCenter: parent.horizontalCenter; spacing: 14; topPadding: 8
Btn { k: Input.g.back; text: "Back to the menu"; height: 52; onClicked: app.act("back") }
Btn { visible: app.restartNeeded; k: Input.g.ok; text: "Restart now"; primary: true; height: 52; onClicked: app.backend.restart() } }
}
}
+13
View File
@@ -0,0 +1,13 @@
// A button name (A, Enter, OK...), round for a controller's face buttons.
import QtQuick
Rectangle {
id: glyph
property string k
property bool dark: false
height: 26; width: Math.max(26, gt.implicitWidth + 14)
radius: (Input.type === "controller" || Input.type === "steam") && k.length === 1 ? 13 : 7
color: dark ? Theme.ink : Theme.key
Text { id: gt; anchors.centerIn: parent; text: glyph.k; color: glyph.dark ? Theme.accent : Theme.textHi
font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold }
}
+25
View File
@@ -0,0 +1,25 @@
// Logo, the screen's name (the version on the menu) and the machine.
import QtQuick
Rectangle {
required property AppState app
required property string iconUrl
readonly property var titles: ({ review: "Review", password: "Password", applying: app.biosRun ? "BIOS update" : "Applying",
done: "Done", bios1: "BIOS update", bios2: "BIOS update" })
width: parent.width; height: 72; color: Theme.bar
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: Theme.line }
Row {
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 14
Image { source: iconUrl; width: 40; height: 40; sourceSize: Qt.size(80, 80); anchors.verticalCenter: parent.verticalCenter }
Text { text: "Steamify"; color: Theme.text; font.family: Theme.display; font.pixelSize: 28; font.weight: Font.Bold; anchors.verticalCenter: parent.verticalCenter }
Text { readonly property bool menu: app.screen === "menu"
text: menu ? "v" + (app.status.version || "") : "/ " + titles[app.screen]
color: Theme.faint; font.family: menu ? Theme.mono : Theme.body; font.pixelSize: menu ? 13 : 15; anchors.verticalCenter: parent.verticalCenter }
}
Rectangle {
visible: app.status.steamMachine === true
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter
height: 34; radius: 17; color: "#1b2330"; width: mt.implicitWidth + 28
Text { id: mt; anchors.centerIn: parent; text: "Steam Machine · CachyOS"; color: Theme.label; font.family: Theme.body; font.pixelSize: 14 }
}
}
+19
View File
@@ -0,0 +1,19 @@
// The input in use (controller, keyboard or remote) and the button names
// shown for it.
pragma Singleton
import QtQuick
QtObject {
property string type: "keyboard"
// Started from Steam: keys come from a Steam Controller through Steam's
// desktop layout (A = Enter, B = Esc, Y = Space, X = Steam's keyboard),
// shown as "steam" with the controller's buttons.
property bool fromSteam: false
readonly property var glyphs: ({
controller: { toggle: "A", choose: "◀ ▶", reapply: "Y", quit: "", apply: "X", ok: "A", back: "B" },
keyboard: { toggle: "Enter", choose: "← →", reapply: "R", quit: "Ctrl+Q", apply: "Ctrl+Enter", ok: "Enter", back: "Esc" },
steam: { toggle: "A", choose: "◀ ▶", reapply: "", quit: "", apply: "↓ A", ok: "A", back: "B" },
remote: { toggle: "OK", choose: "◀ ▶", reapply: "Red", quit: "", apply: "↓ Apply", ok: "OK", back: "Back" }
})
readonly property var g: glyphs[type]
}
+58
View File
@@ -0,0 +1,58 @@
// Steamify v2: the window. The state and logic are in AppState.qml, each
// screen in its own file (menu -> review -> password -> applying -> done,
// and the BIOS warnings). Designed at 1280x720 and scaled to the window, so
// it fits a TV too.
import QtQuick
import QtQuick.Window
import QtQuick.Controls.Basic
ApplicationWindow {
id: win
required property var backend
required property var gamepad
required property bool fullscreen
required property string iconUrl
required property bool fromSteam
Component.onCompleted: Input.fromSteam = fromSteam
width: 1280; height: 720
minimumWidth: 960; minimumHeight: 540
visibility: fullscreen ? Window.FullScreen : Window.Windowed
title: "Steamify"
color: Theme.bg
AppState {
id: steamify
backend: win.backend
gamepad: win.gamepad
// Keys go back to the screen when a text field's screen closes.
onScreenChanged: if (screen !== "password" && screen !== "bios2") stage.forceActiveFocus()
}
// The 1280x720 stage, scaled to the window: stretched to the window's
// own shape, so the bars reach the edges and the content fills it.
Item {
id: stage
readonly property real k: Math.min(win.width / 1280, win.height / 720)
width: win.width / k; height: win.height / k
transformOrigin: Item.TopLeft
scale: k
focus: true
Keys.onPressed: function (e) { steamify.keyPressed(e); }
Keys.onReleased: function (e) { steamify.keyReleased(e); }
Header { id: header; app: steamify; iconUrl: win.iconUrl }
Item {
id: content
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width
MenuScreen { anchors.fill: parent; app: steamify; visible: steamify.screen === "menu" }
ReviewScreen { anchors.fill: parent; app: steamify; visible: steamify.screen === "review" }
PasswordScreen { anchors.fill: parent; app: steamify; visible: steamify.screen === "password" }
ApplyingScreen { anchors.fill: parent; app: steamify; visible: steamify.screen === "applying" }
BiosWarningScreen { anchors.fill: parent; app: steamify; visible: steamify.screen === "bios1" }
BiosConfirmScreen { anchors.fill: parent; app: steamify; visible: steamify.screen === "bios2" }
DoneScreen { anchors.fill: parent; app: steamify; visible: steamify.screen === "done" }
}
}
}
+80
View File
@@ -0,0 +1,80 @@
// One menu item: name, badge and hint on the left; its control (switch,
// Gaming/Desktop choice or action button) on the right. Sub-items fold in
// and out with their parent.
import QtQuick
Item {
id: row
required property var modelData
required property int index
required property AppState app
readonly property var it: modelData
readonly property bool shown: !it.parent || !!app.want[it.parent]
readonly property int rowIndex: app.rows.findIndex(function (r) { return r.id === it.id; })
readonly property bool selected: shown && rowIndex === app.sel
readonly property bool on: !!app.want[it.id]
// Greyed out: an action with nothing to do, or an option this machine
// can't turn on.
readonly property bool unavailable: (it.kind === "action" && !(app.bios && app.bios.selectable)) || (it.kind === "toggle" && it.selectable === false)
readonly property string hint: it.id === "bios" ? app.biosHint()
: it.selectable === false ? it.hint
: ((Texts.items[it.id] || {}).hint || it.hint)
height: shown ? 62 : 0
opacity: shown ? 1 : 0
clip: true
Behavior on height { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 160 } }
Rectangle {
width: parent.width; height: 56; radius: 12
color: row.selected ? Theme.cardSel : Theme.card
border.width: row.selected ? 2 : 0; border.color: Theme.accent
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onClicked: { Input.type = "keyboard"; if (row.selected) app.act("accept"); else app.sel = row.rowIndex; } }
Text { visible: !!row.it.parent; x: 20; anchors.verticalCenter: parent.verticalCenter; text: "└"; color: "#56627a"; font.pixelSize: 18 }
Column {
anchors.verticalCenter: parent.verticalCenter; spacing: 2
x: row.it.parent ? 46 : 16
width: controls.x - x - 16
opacity: row.unavailable ? 0.6 : 1
Row { width: parent.width; spacing: 10
Text { text: app.label(row.it); color: Theme.textHi; font.family: Theme.body; font.pixelSize: 17; font.weight: Font.DemiBold; elide: Text.ElideRight
width: Math.min(implicitWidth, parent.width - (badge.visible ? badge.width + parent.spacing : 0)) }
Badge { id: badge; update: !!row.it.update; isNew: !!row.it["new"]; anchors.verticalCenter: parent.verticalCenter } }
Text { text: row.hint; color: Theme.mute; font.family: Theme.body; font.pixelSize: 13; elide: Text.ElideRight; width: parent.width }
}
// Every control ends on the same edge.
Item {
id: controls
opacity: row.it.kind === "toggle" && row.it.selectable === false ? 0.4 : 1
anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter
width: 180; height: 32
// choice
Rectangle { visible: row.it.kind === "choice"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
width: 172; height: 32; radius: 10; color: Theme.bg
Row { anchors.centerIn: parent; spacing: 4
Repeater { model: [["gamescope", "Gaming"], ["desktop", "Desktop"]]
Rectangle { required property var modelData
readonly property bool picked: app.boot === modelData[0]
width: 80; height: 26; radius: 7; color: picked ? Theme.accent : "transparent"
Text { anchors.centerIn: parent; text: parent.modelData[1]; color: parent.picked ? Theme.ink : Theme.mute
font.family: Theme.body; font.pixelSize: 13; font.weight: Font.DemiBold }
MouseArea { anchors.fill: parent; onClicked: app.boot = parent.modelData[0] } } } } }
// toggle: "now on/off" left of the switch
Rectangle { id: sw; visible: row.it.kind === "toggle"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
width: 46; height: 26; radius: 13; color: row.on ? Theme.accent : "#343f50"
Behavior on color { ColorAnimation { duration: 120 } }
Rectangle { width: 20; height: 20; radius: 10; color: "#f4f7fb"; y: 3; x: row.on ? 23 : 3; Behavior on x { NumberAnimation { duration: 120 } } } }
Text { visible: row.it.kind === "toggle"; anchors.right: sw.left; anchors.rightMargin: 12; anchors.verticalCenter: parent.verticalCenter
text: row.it.on ? "now on" : "now off"; color: row.it.on ? Theme.good : Theme.faint; font.family: Theme.mono; font.pixelSize: 12 }
// action (Update BIOS)
Rectangle { visible: row.it.kind === "action"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
width: at.implicitWidth + 24; height: 30; radius: 8; color: Theme.warnBg
Text { id: at; anchors.centerIn: parent; color: Theme.warn; font.family: Theme.body; font.pixelSize: 13; font.weight: Font.DemiBold
text: app.biosChecking ? "Checking…" : app.bios && app.bios.selectable ? "Update…" : (app.bios && app.bios.newest ? "Up to date" : "Unavailable") } }
}
}
}
+80
View File
@@ -0,0 +1,80 @@
// The menu: every item on the left, what the selected one does and the
// system on the right, Review & apply at the bottom.
import QtQuick
import QtQuick.Controls.Basic
Item {
id: screen
required property AppState app
readonly property var g: Input.g
Text {
visible: app.items.length === 0
anchors.centerIn: parent; color: Theme.mute; font.family: Theme.body; font.pixelSize: 18
text: app.status.error ? "Couldn't read the current state:\n" + app.status.error : "Checking what's on…"
horizontalAlignment: Text.AlignHCenter
}
Item {
visible: app.items.length > 0
x: 40; y: 28; width: parent.width - 80; height: parent.height - 28 - 64 - 20
Column {
width: 740; height: parent.height; spacing: 10
Text { text: "What do you want?"; color: Theme.text; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold }
ListView {
id: list; width: parent.width; height: parent.height - 50; clip: true
// A fixed model: sub-items fold in and out instead of the
// list being rebuilt (and jumping) on every tick.
model: app.items
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
delegate: MenuRow {
app: screen.app
width: list.width - 12; x: 2
onSelectedChanged: if (selected) Qt.callLater(list.positionViewAtIndex, index, ListView.Contain)
}
}
}
Column {
x: 772; width: parent.width - 772; height: parent.height; spacing: 16
DetailPanel { app: screen.app; width: parent.width; height: parent.height - sys.height - 16 }
Rectangle {
id: sys; width: parent.width; height: 82; radius: 16; color: Theme.card
Grid { anchors.fill: parent; anchors.margins: 16; anchors.leftMargin: 20; anchors.rightMargin: 20; columns: 2; columnSpacing: 20; rowSpacing: 10
Repeater {
model: [["Kernel", (app.status.kernel || "").replace("-cachyos", "")],
["LED bar", app.status.steamMachine ? (app.status.leds || 0) + " LEDs" : "—"],
["HDMI-CEC", app.status.cecDevices ? "/dev/" + app.status.cecDevices.split(" ")[0] : "none"],
["Version", app.status.version || ""]]
Row { required property var modelData; width: (sys.width - 60) / 2
Text { text: parent.modelData[0]; color: Theme.faint; font.family: Theme.body; font.pixelSize: 13; width: parent.width / 2 }
Text { text: parent.modelData[1]; color: Theme.text; font.family: Theme.mono; font.pixelSize: 13; width: parent.width / 2
horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } }
}
}
}
}
}
Bar {
Row {
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 28
Repeater {
// Quit only has a key on a keyboard (Ctrl+Q), and re-apply has
// none through Steam's layout; a controller quits through Steam.
model: [[screen.g.toggle, "Toggle"], [screen.g.choose, "Choose"], [screen.g.reapply, "Re-apply what's on"], [screen.g.quit, "Quit"]]
.filter(function (h) { return h[0] !== ""; })
Row { required property var modelData; spacing: 8
Glyph { k: parent.modelData[0]; anchors.verticalCenter: parent.verticalCenter }
Text { text: parent.modelData[1]; color: Theme.label; font.family: Theme.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter } }
}
}
Row {
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 16
Text { readonly property int n: app.computePlan().length
text: n === 0 ? "Everything is the way you want it" : n + (n === 1 ? " change" : " changes")
color: Theme.faint; font.family: Theme.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter }
Btn { k: screen.g.apply; text: "Review & apply"; primary: true; enabled: app.canApply
focusRing: app.sel === app.rows.length; height: 44; onClicked: app.goReview(false) }
}
}
}
+57
View File
@@ -0,0 +1,57 @@
// The sudo password, asked once per run; never stored.
import QtQuick
import QtQuick.Controls.Basic
Rectangle {
id: screen
required property AppState app
color: "#0c1016"
function submit() { var p = pw.text; pw.text = ""; app.submitPassword(p); }
Connections {
target: screen.app
function onPasswordRequested() { pw.text = ""; pw.shown = false; pw.forceActiveFocus(); }
function onPasswordRejected() { pw.forceActiveFocus(); }
function onPasswordSubmitRequested() { screen.submit(); }
}
Binding { target: screen.app; property: "typing"; value: pw.activeFocus; when: screen.visible }
Rectangle {
anchors.centerIn: parent; width: 560; height: col.implicitHeight + 72; radius: 20; color: Theme.card; border.width: 1; border.color: Theme.line
Column {
id: col; anchors.fill: parent; anchors.margins: 36; anchors.leftMargin: 40; anchors.rightMargin: 40; spacing: 18
Row { spacing: 14
Rectangle { width: 48; height: 48; radius: 14; color: Theme.infoBg
// A padlock.
Canvas { anchors.centerIn: parent; width: 24; height: 24
onPaint: { var c = getContext("2d"); c.strokeStyle = Theme.info; c.lineWidth = 1.8; c.beginPath(); c.roundedRect(4.5, 10.5, 15, 10, 2.5, 2.5); c.stroke();
c.beginPath(); c.moveTo(8, 10.5); c.lineTo(8, 8); c.arc(12, 8, 4, Math.PI, 0); c.lineTo(16, 10.5); c.stroke(); } } }
Column { spacing: 2; anchors.verticalCenter: parent.verticalCenter
Text { text: "Your password, once"; color: Theme.text; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold }
Text { text: "Steamify needs it to change system settings."; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14 } }
}
Text { text: "Password for " + app.backend.user(); color: Theme.soft; font.family: Theme.body; font.pixelSize: 14; font.weight: Font.DemiBold }
TextField {
id: pw; width: parent.width; height: 52; echoMode: shown ? TextInput.Normal : TextInput.Password
property bool shown: false
color: Theme.textHi; font.family: Theme.body; font.pixelSize: 18; leftPadding: 16; rightPadding: 56
placeholderText: "Password"; placeholderTextColor: "#6b778a"
background: Rectangle { radius: 12; color: Theme.bg; border.width: 2; border.color: app.wrongPassword ? Theme.bad : Theme.accent }
Keys.onReturnPressed: screen.submit()
Keys.onEnterPressed: screen.submit()
Keys.onEscapePressed: app.act("back")
Text { anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter; text: pw.shown ? "Hide" : "Show"
color: Theme.label; font.family: Theme.body; font.pixelSize: 13
MouseArea { anchors.fill: parent; anchors.margins: -10; onClicked: pw.shown = !pw.shown } }
}
Text { visible: app.wrongPassword; text: "That password didn't work. Try again."; color: Theme.bad; font.family: Theme.body; font.pixelSize: 14 }
Rectangle { width: parent.width; height: 46; radius: 12; color: Theme.note
Text { anchors.verticalCenter: parent.verticalCenter; x: 14; color: Theme.soft; font.family: Theme.body; font.pixelSize: 14
text: ({ controller: "Press Steam + X for the on-screen keyboard.", keyboard: "Type your password and press Enter.", steam: "Press X for Steam's on-screen keyboard, type your password and press A.", remote: "Select the field for the on-screen keyboard." })[Input.type] } }
Item { width: parent.width; height: 48
Btn { k: Input.g.back; text: "Back"; onClicked: app.act("back") }
Btn { anchors.right: parent.right; k: Input.g.ok; text: "Apply"; primary: true; onClicked: screen.submit() } }
Text { text: "Only used for this run. Steamify never stores it."; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; anchors.horizontalCenter: parent.horizontalCenter }
}
}
}
+49
View File
@@ -0,0 +1,49 @@
// What a run will change, with a summary; Apply asks for the password.
import QtQuick
import QtQuick.Controls.Basic
Item {
required property AppState app
Column {
x: 40; y: 32; width: 740; spacing: 14
Text { text: app.plan.length === 0 ? "Nothing to change" : "This will"; color: Theme.text; font.family: Theme.display; font.pixelSize: 34; font.weight: Font.DemiBold }
ListView {
width: 740; clip: true; spacing: 8
height: Math.min(contentHeight, parent.parent.height - 32 - 50 - 14 - 66 - 64 - 32)
model: app.plan
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
delegate: Rectangle {
id: step
required property var modelData
readonly property var st: Texts.actions[modelData.action] || ["", Theme.text, Theme.card]
width: 740; height: 56; radius: 12; color: Theme.card
Row { anchors.fill: parent; anchors.leftMargin: 18; spacing: 14
Chip { text: step.st[0]; fg: step.st[1]; bgc: step.st[2]; width: 84; anchors.verticalCenter: parent.verticalCenter }
Text { text: Texts.label(step.modelData.id); color: Theme.textHi; font.family: Theme.body; font.pixelSize: 17; font.weight: Font.DemiBold
anchors.verticalCenter: parent.verticalCenter }
Badge { isNew: !!step.modelData.isNew; anchors.verticalCenter: parent.verticalCenter }
}
}
}
Rectangle { width: 740; height: 52; radius: 12; color: Theme.note
Text { anchors.verticalCenter: parent.verticalCenter; x: 18; text: "Your password is asked once. Changes to how the PC starts need a restart."
color: Theme.soft; font.family: Theme.body; font.pixelSize: 14 } }
}
Rectangle {
x: parent.width - 468; y: 32; width: 428; height: 250; radius: 16; color: Theme.card
Column { anchors.fill: parent; anchors.margins: 24; spacing: 12
Text { text: "SUMMARY"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
Repeater { model: [["Turn on", "on"], ["Update", "update"], ["Re-apply", "again"], ["Turn off", "off"]]
Row { required property var modelData; width: 380
Text { text: parent.modelData[0]; color: Theme.soft; font.family: Theme.body; font.pixelSize: 15; width: 300 }
Text { readonly property string action: parent.modelData[1]
text: app.plan.filter(function (p) { return p.action === action; }).length
color: Theme.text; font.family: Theme.mono; font.pixelSize: 15; width: 80; horizontalAlignment: Text.AlignRight } } }
}
}
Bar {
Btn { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: Input.g.back; text: "Back to the menu"; onClicked: app.act("back") }
Btn { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: Input.g.ok; text: "Apply"; primary: true
visible: app.plan.length > 0; onClicked: app.onApplyPressed() }
}
}
+17
View File
@@ -0,0 +1,17 @@
// The BIOS update's risks.
import QtQuick
Column {
id: list
spacing: 8
Repeater {
model: ["A failed or interrupted BIOS update can leave the machine unable to start (bricked). Steamify, CachyOS and Valve take no responsibility for that.",
"NEVER turn off the power, unplug the machine or press the power button while it updates, including the restart(s) afterwards.",
"The screen can stay black for several minutes. Wait.",
"Only on mains power, with all other programs closed."]
Row { required property string modelData; spacing: 10; width: list.width
Rectangle { width: 6; height: 6; radius: 3; color: Theme.bad; y: 9 }
Text { text: parent.modelData; width: parent.width - 16; wrapMode: Text.WordWrap; color: Theme.soft
font.family: Theme.body; font.pixelSize: 15; lineHeight: 1.3 } }
}
}
+88
View File
@@ -0,0 +1,88 @@
// Texts per item; the backend's labels are the fallback.
pragma Singleton
import QtQuick
QtObject {
readonly property var items: ({
gaming: { label: "SteamOS conversion", hint: "Boot into gaming mode, Steam on the desktop",
body: "Boots straight into gaming mode. Switch to Desktop in Steam works, and Return to Gaming Mode on the desktop brings you back.",
changes: ["Autologin into gamescope", "Return to Gaming Mode shortcut"] },
boot: { label: "Boot into", hint: "Where the PC starts",
body: "Where the PC starts after a restart. Switching back and forth works the same either way.",
changes: ["Desktop: the Plasma session is set before login", "Gaming: the SteamOS default"] },
silent: { label: "Start Steam silently in desktop mode", hint: "In the tray at login on the desktop, no window",
body: "Steam starts by itself when you log in to the Plasma desktop, in the tray without a window. Gaming mode starts its own Steam.",
changes: ["Steam is started with -silent (a systemd user service, desktop only)", "Off: the service is removed again"] },
nvidia: { label: "Gaming on NVIDIA", hint: "Steam on the desktop, started at login",
body: "gamescope's own gaming mode shows a corrupted picture on NVIDIA graphics cards (an open NVIDIA driver bug), so it isn't offered here. This puts Steam on the Plasma desktop instead and starts it when you log in.",
changes: ["Steam is installed when it's missing", "Steam starts at login (a systemd user service)", "Off: the service is removed again; Steam is only removed if Steamify installed it"] },
bigpicture: { label: "Steam starts in Big Picture", hint: "The controller-friendly Steam, or the normal window",
body: "Steam opens in Big Picture at login, which works well with a controller on the TV. Untick it for Steam's normal window.",
changes: ["Steam is started with -gamepadui", "Plasma starts with an empty session, so old windows don't cover Big Picture", "Off: Steam starts in its normal window"] },
nvsilent: { label: "Start Steam silently", hint: "In the tray at login, no window",
body: "Steam starts in the tray at login without a window. Not together with Big Picture: ticking one unticks the other.",
changes: ["Steam is started with -silent", "Off: Steam starts in its normal window"] },
theme: { label: "SteamOS theme", hint: "Vapor look for the desktop, Add to Steam",
body: "The Vapor look of SteamOS for the desktop, with its panel, launcher icon and wallpaper.",
changes: ["cachyos-vapor global theme", "Add to Steam in right-click menus", "Nested Desktop, Steam keyboard window rule"] },
glyphs: { label: "Steam Deck/Machine icons", hint: "Deck button icons in gaming mode",
body: "Steam shows Steam Deck button icons in gaming mode.", changes: ["STEAM_GAMEPADUI_ARGS -steamos3"] },
single: { label: "Single user mode", hint: "No password, lock screen or wallet prompts",
body: "Like SteamOS: never a login or lock screen, and no KDE wallet password prompts. Typing a password with a controller is no fun.",
changes: ["SDDM autologin, no lock screen or user switching", "Valve's empty wallet; your own is kept aside", "Launcher shows Sleep, Restart, Shut Down only"] },
launcher: { label: "Steamify shortcut", hint: "The app on the desktop and in the launcher",
body: "Opens the newest Steamify app, so you never need the install command again. Steamify Terminal in the launcher opens the terminal menu.",
changes: ["Steamify CachyOS on the desktop and in the launcher (the app)", "Steamify Terminal in the launcher"] },
steamgame: { label: "Add as non-Steam game", hint: "Steamify in your Steam library",
body: "Adds Steamify to your Steam library as a non-Steam game. Started from Steam it gets your controller (a Steam Controller only talks to Steam while Steam runs), and you can open it in gaming mode. Steam closes for a moment and starts again, since it only reads its games list at startup: so this is done from the desktop, not from gaming mode or with Steamify started from Steam.",
changes: ["Steamify CachyOS in your Steam library, with its icon", "An entry that's already there is updated, not added twice", "Off (or the shortcut off): removed from the library again"] },
notify: { label: "Update notifications", hint: "A notification when there's a new Steamify",
body: "Checks once a day and when you log in to the desktop. When there's a new Steamify you get a notification and a tray icon: open the app to see what's new and choose what to update, or skip that version. It never updates anything by itself.",
changes: ["A daily check (systemd user timer), only on the desktop", "Tray icon only while an update is waiting", "Skip this version: quiet until the next one"] },
extended_controller_support: { label: "Extended controller support", hint: "Xbox wireless dongle, Xbox controllers over Bluetooth",
body: "Drivers for Xbox controllers that Linux doesn't handle well by itself. xone makes the Xbox wireless dongle work (the kernel has no driver for it), xpadneo gives an Xbox controller over Bluetooth rumble including the triggers, the right button mapping and its battery level. They are built for every installed kernel, which takes a minute or two each.",
changes: ["xone (the dongle) with its firmware, and xpadneo (Bluetooth), from the CachyOS repo", "Kernel headers for every installed kernel, so the drivers build for each", "Off: only what Steamify installed is removed again"] },
vram: { label: "VRAM booster", hint: "The game in front keeps its VRAM",
body: "Like SteamOS 3.9: the game you're playing keeps its video memory, and background apps are moved out first. Without it a game that needs most of the video memory can spill into system RAM and stutter. Shown for GPUs whose driver supports it (AMD, Intel, NVIDIA with its open kernel modules from driver 615) on kernel 7.2 or newer.",
changes: ["dmemcg-booster (system and user service)", "plasma-foreground-booster, which tells it which window is in front", "Off: removed again, unless you had installed them yourself"] },
cec: { label: "HDMI-CEC", hint: "Use Steam with the TV remote, TV on/off with the PC", experimental: true,
body: "Use Steam with your TV remote, and the TV turns on and off with the PC. Turn on CEC on the TV too (Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink).",
changes: ["Valve's cecd and cec-audio-control", "HDMI CEC section in Steam's Display settings", "Volume buttons for the TV in the Quick Access menu"] },
machine: { label: "Steam Machine support", hint: "LED bar, fan and performance settings in Steam",
body: "The front LED bar works, Steam's hardware settings work, and the power button puts it to sleep like a console.",
changes: ["leds-valve driver for every kernel (DKMS)", "steamos-manager for Steam's settings", "Console-like power handling"] },
poweroff: { label: "Power-off fix", hint: "Stays off after shutting down",
body: "With recent kernels the Steam Machine starts again right after shutting down: the firmware leaves a wake bit set, and newer kernels (7.2, and updates of 6.x, 7.0 and 7.1) no longer clear it. Valve's own kernel clears it; this small module does the same right before power-off.",
changes: ["steamify-fremont-poweroff module for each installed kernel (DKMS)", "Only on a Steam Machine, only touches that one wake bit", "Off: recent kernels may start it again after shutting down"] },
kpin: { label: "Pin the kernel", hint: "No longer needed: untick for CachyOS's current kernel",
body: "Kept the Steam Machine on CachyOS kernel 7.1.6, which powered off properly. The power-off fix does that on every kernel now, so the pin is removed and CachyOS's current kernel comes back.",
changes: ["Removed from IgnorePkg", "linux-cachyos updated to CachyOS's current version"] },
hdmi: { label: "HDMI refresh boost", hint: "No longer needed: removed",
body: "Made HDMI displays run above 60 Hz on the pinned kernel. Current kernels do that themselves, so what's left of it is removed and every display uses its own EDID again.",
changes: ["Saved display EDIDs and the hotplug service removed", "The old kernel parameter removed, if it's still there"] },
bios: { label: "Update BIOS", hint: "",
body: "Installs Valve's newest Steam Machine BIOS, at your own risk. It checks Valve's checksum and asks fwupd whether the file fits this machine, then warns you twice before anything is written.",
changes: ["Valve's fremont-hw-support package (checksum checked)", "fwupd writes the BIOS during the next restart", "Keep the power on until the machine has fully started again"] }
})
// Replaces the explanation of an option that can't be turned on here.
readonly property var unsupported: ({
// The body comes from the backend (item.note): it depends on the driver.
vram: { changes: ["Nothing until then"] }
})
// Per plan action: the review's chip [text, colour, background] and the
// progress screen's verb.
readonly property var actions: ({
on: ["Turn on", Theme.good, Theme.goodBg, "Turn on "], off: ["Turn off", Theme.bad, Theme.badBg, "Turn off "],
again: ["Re-apply", Theme.info, Theme.infoBg, "Re-apply "], update: ["Update", Theme.warn, Theme.warnBg, ""],
desktop: ["Desktop", Theme.info, Theme.infoBg, "Boot into "], gaming: ["Gaming", Theme.info, Theme.infoBg, "Boot into "],
check: ["Check", Theme.warn, Theme.warnBg, "Download and check the "], flash: ["Flash", Theme.bad, Theme.badBg, "Hand to fwupd: the "]
})
function of(item) {
var tx = items[item.id] || {};
if (item.selectable !== false || !unsupported[item.id]) return tx;
return Object.assign({}, tx, unsupported[item.id], item.note ? { body: item.note } : {});
}
function label(id, fallback) { return (items[id] || {}).label || fallback || id; }
}
+36
View File
@@ -0,0 +1,36 @@
// Colours and fonts (from the design).
pragma Singleton
import QtQuick
QtObject {
readonly property color bg: "#0f1319"
readonly property color bar: "#141a22"
readonly property color line: "#232c38"
readonly property color card: "#161d27"
readonly property color cardSel: "#1f2a3a"
readonly property color text: "#e6ebf2"
readonly property color textHi: "#eef2f7"
readonly property color soft: "#c3ccd8"
readonly property color mute: "#97a3b4"
readonly property color faint: "#8b97a8"
readonly property color label: "#b8c3d1"
readonly property color accent: "#3ea6ff"
readonly property color ink: "#06121f"
readonly property color good: "#7fd1a8"
readonly property color goodBg: "#173226"
readonly property color warn: "#f2a33a"
readonly property color warnBg: "#2a2116"
readonly property color bad: "#ff9a8a"
readonly property color badBg: "#2a1c1c"
readonly property color info: "#7cc4ff"
readonly property color infoBg: "#1b2b40"
readonly property color note: "#1a2230"
readonly property color key: "#2a3444"
readonly property color button: "#232c38"
readonly property color danger: "#8a2c2c"
readonly property color dangerBg: "#1d1416"
readonly property color dangerLine: "#b33a3a"
readonly property string display: "Barlow Semi Condensed"
readonly property string body: "IBM Plex Sans"
readonly property string mono: "IBM Plex Mono"
}
+20
View File
@@ -0,0 +1,20 @@
singleton Theme 1.0 Theme.qml
singleton Texts 1.0 Texts.qml
singleton Input 1.0 Input.qml
AppState 1.0 AppState.qml
Glyph 1.0 Glyph.qml
Btn 1.0 Btn.qml
Chip 1.0 Chip.qml
Badge 1.0 Badge.qml
Bar 1.0 Bar.qml
RiskList 1.0 RiskList.qml
Header 1.0 Header.qml
MenuRow 1.0 MenuRow.qml
DetailPanel 1.0 DetailPanel.qml
MenuScreen 1.0 MenuScreen.qml
ReviewScreen 1.0 ReviewScreen.qml
PasswordScreen 1.0 PasswordScreen.qml
ApplyingScreen 1.0 ApplyingScreen.qml
BiosWarningScreen 1.0 BiosWarningScreen.qml
BiosConfirmScreen 1.0 BiosConfirmScreen.qml
DoneScreen 1.0 DoneScreen.qml
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
# Steamify CachyOS - get and start the app, for `curl | bash`:
# curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify-app.sh | bash
# Downloads the newest release's app package (steamify-app.tar.gz: the app,
# steamify.sh, lib/, patches/ and services/) to ~/.local/share/steamify/app, installs PySide6 if
# it's missing, adds a launcher entry, and starts the app.
# STEAMIFY_RELEASE=<tag> takes that release instead of the newest.
# STEAMIFY_BRANCH=<branch> takes that branch's source instead (for testing
# before a build).
set -euo pipefail
REPO="theupriser/steamify-cachyos"
RELEASE="${STEAMIFY_RELEASE:-latest}"
DEST="${XDG_DATA_HOME:-$HOME/.local/share}/steamify/app"
APPS="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
say() { printf '\033[36m[INFO]\033[0m %s\n' "$*"; }
die() { printf '\033[31m[ERROR] %s\033[0m\n' "$*" >&2; exit 1; }
[[ $EUID -ne 0 ]] || die "Run this as your normal user, not as root."
command -v pacman >/dev/null || die "This is for CachyOS/Arch (no pacman found)."
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
if [[ -n "${STEAMIFY_BRANCH:-}" ]]; then
say "Downloading Steamify from the $STEAMIFY_BRANCH branch..."
curl -fsSL "https://github.com/$REPO/archive/refs/heads/$STEAMIFY_BRANCH.tar.gz" -o "$tmp/src.tar.gz" ||
die "Couldn't download the $STEAMIFY_BRANCH branch."
mkdir "$tmp/app"
tar -xzf "$tmp/src.tar.gz" -C "$tmp/app" --strip-components=1
else
if [[ "$RELEASE" == latest ]]; then
url="https://github.com/$REPO/releases/latest/download/steamify-app.tar.gz"
say "Downloading the newest Steamify app..."
else
url="https://github.com/$REPO/releases/download/$RELEASE/steamify-app.tar.gz"
say "Downloading the Steamify app ($RELEASE)..."
fi
curl -fsSL "$url" -o "$tmp/app.tar.gz" ||
die "Couldn't download the app (is there a release with steamify-app.tar.gz yet?)."
mkdir "$tmp/app"
tar -xzf "$tmp/app.tar.gz" -C "$tmp/app"
fi
[[ -x "$tmp/app/ui/steamify-ui" && -f "$tmp/app/steamify.sh" ]] || die "The download doesn't contain the app."
mkdir -p "$(dirname "$DEST")"
rm -rf "$DEST.new"; mv "$tmp/app" "$DEST.new"
rm -rf "$DEST"; mv "$DEST.new" "$DEST"
say "Installed to $DEST ($(grep -m1 -oP '^VERSION=\K.*' "$DEST/steamify.sh"))."
if ! python3 -c 'import PySide6.QtQml' 2>/dev/null; then
say "The app needs PySide6 (Qt for Python); installing it..."
# Piped in via curl, stdin isn't the terminal: sudo asks on it anyway.
sudo pacman -S --needed --noconfirm pyside6 </dev/tty || die "Installing pyside6 failed."
fi
# Launcher entry (its id also names the app to the desktop portal). The
# Steamify shortcut's entry has the same id and always starts the newest
# release: left alone.
mkdir -p "$APPS"
grep -q '^X-Steamify-Shortcut=true' "$APPS/steamify-ui.desktop" 2>/dev/null ||
cat > "$APPS/steamify-ui.desktop" << EOF
[Desktop Entry]
Type=Application
Name=Steamify CachyOS
Comment=Turn this PC into a SteamOS-style console
Exec=$DEST/ui/steamify-ui
Icon=$DEST/assets/steam-gaming-settings.svg
Terminal=false
Categories=Game;Settings;
EOF
command -v update-desktop-database >/dev/null && update-desktop-database "$APPS" 2>/dev/null || true
say "Starting Steamify..."
# Started from Steam: stay in the foreground, or Steam counts the game as
# ended right away and stops giving it the controller.
if [[ -n "${SteamGameId:-}" ]]; then
exec "$DEST/ui/steamify-ui" < /dev/null
fi
setsid "$DEST/ui/steamify-ui" >/dev/null 2>&1 < /dev/null &
Executable
+389
View File
@@ -0,0 +1,389 @@
#!/usr/bin/env python3
"""Steamify CachyOS - graphical app (v2).
A controller-, TV remote- and keyboard-friendly front end for steamify.sh:
the components and their logic stay in lib/*.sh, reached through
`steamify.sh --backend status|apply` (lib/backend.sh). Needs PySide6
(pacman -S pyside6).
steamify-ui [--windowed|--fullscreen]
"""
import json
import os
import select
import shutil
import stat
import struct
import sys
import tempfile
import threading
import time
# Started from Steam (a non-Steam game, so Steam Input hands it the
# controller): Steam preloads its overlay into every game, and Qt aborts with
# it loaded. Start again without it, before Qt is imported.
if "gameoverlayrenderer" in os.environ.get("LD_PRELOAD", ""):
os.environ["LD_PRELOAD"] = ":".join(p for p in os.environ["LD_PRELOAD"].split(":")
if p and "gameoverlayrenderer" not in p)
os.execv(sys.executable, [sys.executable] + sys.argv)
HERE = os.path.dirname(os.path.realpath(__file__))
try:
from PySide6.QtCore import (QObject, QProcess, QProcessEnvironment,
Property, Signal, Slot, QUrl, SLOT)
from PySide6.QtGui import QGuiApplication, QIcon
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtDBus import QDBusConnection
except ImportError:
msg = ("Steamify's app needs PySide6 (Qt for Python).\n"
"Install it with: sudo pacman -S pyside6\n"
"or run the terminal version: steamify.sh")
print(msg, file=sys.stderr)
if shutil.which("kdialog"):
os.system("kdialog --title Steamify --sorry " + json.dumps(msg))
sys.exit(1)
def find_script():
"""steamify.sh next to the app: the repository (../steamify.sh) or an
install that keeps both in one folder."""
for p in (os.path.join(HERE, "..", "steamify.sh"),
os.path.join(HERE, "steamify.sh")):
if os.path.isfile(p):
return os.path.realpath(p)
return None
# --- Controller input -------------------------------------------------------
# Qt 6 has no gamepad module: read the evdev device directly. Controllers
# are tagged uaccess, so the logged-in user may read them.
EV_KEY, EV_ABS = 0x01, 0x03
BUTTONS = {0x130: "a", 0x131: "b", 0x133: "y", 0x134: "x", # SOUTH EAST NORTH WEST
0x13a: "select", 0x13b: "start", 0x13c: "guide",
0x220: "up", 0x221: "down", 0x222: "left", 0x223: "right"} # BTN_DPAD_*
ABS_X, ABS_Y, ABS_HAT0X, ABS_HAT0Y = 0x00, 0x01, 0x10, 0x11
EVENT = struct.Struct("llHHi")
def controller_devices():
"""/dev/input/eventN of every device that has a gamepad's south button."""
found = []
base = "/sys/class/input"
for name in sorted(os.listdir(base)):
if not name.startswith("event"):
continue
try:
with open(os.path.join(base, name, "device", "capabilities", "key")) as f:
words = f.read().split()
except OSError:
continue
bits = 0
for w in words:
bits = (bits << 64) | int(w, 16)
if bits >> 0x130 & 1:
found.append("/dev/input/" + name)
return found
class Gamepad(QObject):
"""Emits button("a"|"b"|"x"|"y"|"up"|...) from any controller, and
"<name>_up" when a button is released."""
button = Signal(str)
def __init__(self):
super().__init__()
self._stop = False
threading.Thread(target=self._run, daemon=True).start()
def _run(self):
files = {}
stick = {ABS_X: 0, ABS_Y: 0}
last_scan = 0
while not self._stop:
if time.time() - last_scan > 2: # plugged in or out
last_scan = time.time()
for path in controller_devices():
if path not in files:
try:
files[path] = open(path, "rb", buffering=0)
except OSError:
pass
if not files:
time.sleep(0.5)
continue
try:
ready, _, _ = select.select(list(files.values()), [], [], 0.5)
except (OSError, ValueError):
ready = []
for f in ready:
try:
data = f.read(EVENT.size * 32)
except OSError:
data = b""
if not data:
files = {p: x for p, x in files.items() if x is not f}
continue
for off in range(0, len(data) - EVENT.size + 1, EVENT.size):
_, _, typ, code, value = EVENT.unpack_from(data, off)
if typ == EV_KEY and code in BUTTONS and value in (0, 1):
# Presses as "a"; releases as "a_up" (the BIOS
# confirmation is a 5-second hold).
self.button.emit(BUTTONS[code] + ("" if value else "_up"))
elif typ == EV_ABS and code in (ABS_HAT0X, ABS_HAT0Y) and value:
self.button.emit({(ABS_HAT0X, -1): "left", (ABS_HAT0X, 1): "right",
(ABS_HAT0Y, -1): "up", (ABS_HAT0Y, 1): "down"}[(code, value)])
elif typ == EV_ABS and code in stick:
# Left stick as a D-pad: one step per push past half-way.
was = stick[code]
now = -1 if value < -16000 else (1 if value > 16000 else 0)
stick[code] = now
if now and not was:
self.button.emit({(ABS_X, -1): "left", (ABS_X, 1): "right",
(ABS_Y, -1): "up", (ABS_Y, 1): "down"}[(code, now)])
# --- Backend ----------------------------------------------------------------
class Backend(QObject):
statusChanged = Signal()
event = Signal("QVariant") # one parsed JSON event from apply
busyChanged = Signal()
remotePressed = Signal() # a key from the TV remote (cecd)
def __init__(self, script):
super().__init__()
self._script = script
self._status = {}
self._busy = False
self._proc = None
self._buf = b""
self._secret_dir = None
# The TV remote's keys reach Qt as keys; cecd also announces them on
# D-Bus, which tells the app they came from the remote.
try:
QDBusConnection.sessionBus().connect(
"com.steampowered.CecDaemon1", "", "com.steampowered.CecDaemon1.CecDevice1",
"UserControlPressed", self, SLOT("_on_remote()"))
except Exception as e: # no remote detection; keys still work
print("steamify-ui: no CEC remote detection:", e, file=sys.stderr)
@Slot()
def _on_remote(self):
self.remotePressed.emit()
def _get_status(self):
return self._status
status = Property("QVariant", _get_status, notify=statusChanged)
def _get_busy(self):
return self._busy
busy = Property(bool, _get_busy, notify=busyChanged)
@Slot(result=str)
def user(self):
return os.environ.get("USER") or os.getlogin()
@Slot()
def refresh(self):
p = QProcess(self)
p.setProgram("bash")
p.setArguments([self._script, "--backend", "status"])
def done(*_):
try:
self._status = json.loads(bytes(p.readAllStandardOutput()).decode())
except ValueError:
self._status = {"error": bytes(p.readAllStandardError()).decode()[-500:]}
self.statusChanged.emit()
p.deleteLater()
p.finished.connect(done)
p.start()
def _make_askpass(self, password):
"""A private folder with a helper that `sudo -A` runs for the
password, and a `sudo` that always uses it (makepkg and yay call
plain sudo). The password is never written to a file: the helper
reads it from a named pipe, and the app writes it into the pipe from
memory each time sudo asks, until _drop_askpass."""
base = os.environ.get("XDG_RUNTIME_DIR") or tempfile.gettempdir()
d = tempfile.mkdtemp(prefix="steamify-", dir=base)
os.chmod(d, stat.S_IRWXU)
pipe = os.path.join(d, "pipe")
os.mkfifo(pipe, 0o600)
askpass = os.path.join(d, "askpass")
with open(askpass, "w") as f:
f.write("#!/bin/sh\nexec cat %s\n" % json.dumps(pipe))
os.chmod(askpass, 0o700)
bindir = os.path.join(d, "bin")
os.mkdir(bindir)
wrapper = os.path.join(bindir, "sudo")
with open(wrapper, "w") as f:
f.write("#!/bin/sh\nexec /usr/bin/sudo -A \"$@\"\n")
os.chmod(wrapper, 0o700)
stop = threading.Event()
data = (password + "\n").encode()
def serve():
while not stop.is_set():
try:
fd = os.open(pipe, os.O_WRONLY) # waits for a reader
except OSError:
return
try:
if not stop.is_set():
os.write(fd, data)
except OSError:
pass
finally:
os.close(fd)
threading.Thread(target=serve, daemon=True).start()
self._askpass_stop = (stop, pipe)
return d, askpass, bindir
def _drop_askpass(self):
if getattr(self, "_askpass_stop", None):
stop, pipe = self._askpass_stop
self._askpass_stop = None
stop.set()
try: # wakes the writer waiting for a reader, which then ends
os.close(os.open(pipe, os.O_RDONLY | os.O_NONBLOCK))
except OSError:
pass
if self._secret_dir:
shutil.rmtree(self._secret_dir, ignore_errors=True)
self._secret_dir = None
def _start(self, args, password):
if self._busy:
return
env = QProcessEnvironment.systemEnvironment()
if password:
self._secret_dir, askpass, bindir = self._make_askpass(password)
env.insert("SUDO_ASKPASS", askpass)
env.insert("PATH", bindir + ":" + env.value("PATH"))
p = QProcess(self)
p.setProcessEnvironment(env)
p.setProcessChannelMode(QProcess.MergedChannels)
p.setProgram("bash")
p.setArguments([self._script, "--backend"] + args)
self._buf = b""
p.readyReadStandardOutput.connect(lambda: self._read(p))
p.finished.connect(lambda *_: self._finished(p))
self._proc = p
self._busy = True
self.busyChanged.emit()
p.start()
@Slot("QVariant", str, bool, str)
def apply(self, ids, boot, reapply, password):
try:
if hasattr(ids, "toVariant"): # a JS array from QML
ids = ids.toVariant()
args = ["apply"]
if reapply:
args.append("--reapply")
if boot:
args += ["--boot", boot]
self._start(args + [str(i) for i in (ids or [])], password)
except Exception as e:
# Never leave the app on the progress screen.
print("steamify-ui: couldn't start:", e, file=sys.stderr)
self._drop_askpass()
self.event.emit({"event": "finished", "failed": [], "restart": False, "error": "start"})
@Slot(str)
def biosPrepare(self, password):
"""Download, checksum, fwupd check: ends with a bios-ready event."""
self._start(["bios-prepare"], password)
@Slot(str)
def biosFlash(self, password):
"""Only after both warnings in the app."""
self._start(["bios-flash"], password)
def _read(self, p):
self._buf += bytes(p.readAllStandardOutput())
while b"\n" in self._buf:
line, self._buf = self._buf.split(b"\n", 1)
text = line.decode(errors="replace")
try:
ev = json.loads(text)
except ValueError:
ev = {"event": "log", "id": "", "line": text}
if isinstance(ev, dict):
self.event.emit(ev)
def _finished(self, p):
self._read(p)
self._drop_askpass()
self._busy = False
self.busyChanged.emit()
p.deleteLater()
self.refresh()
@Slot(str, result=bool)
def checkPassword(self, password):
"""True if sudo accepts the password (or needs none)."""
import subprocess
if subprocess.run(["sudo", "-n", "true"], capture_output=True).returncode == 0:
return True
r = subprocess.run(["sudo", "-S", "-k", "-p", "", "true"],
input=(password + "\n").encode(), capture_output=True)
return r.returncode == 0
@Slot(result=bool)
def needsPassword(self):
import subprocess
return subprocess.run(["sudo", "-n", "true"], capture_output=True).returncode != 0
@Slot()
def restart(self):
QProcess.startDetached("systemctl", ["reboot"])
def shutdown(self):
if self._proc and self._proc.state() != QProcess.NotRunning:
self._proc.kill()
self._proc.waitForFinished(3000)
self._drop_askpass()
def main():
script = find_script()
if not script:
print("steamify-ui: can't find steamify.sh next to the app.", file=sys.stderr)
return 1
in_gamescope = bool(os.environ.get("GAMESCOPE_WAYLAND_DISPLAY")) or \
os.environ.get("XDG_CURRENT_DESKTOP", "").lower() == "gamescope"
fullscreen = "--fullscreen" in sys.argv or (in_gamescope and "--windowed" not in sys.argv)
app = QGuiApplication(sys.argv)
app.setApplicationName("Steamify")
app.setDesktopFileName("steamify-ui")
icon = os.path.join(HERE, "..", "assets", "steam-gaming-settings.svg")
if os.path.isfile(icon):
app.setWindowIcon(QIcon(icon))
backend = Backend(script)
pad = Gamepad()
engine = QQmlApplicationEngine()
engine.setInitialProperties({"backend": backend, "gamepad": pad,
"fullscreen": fullscreen,
# Started from Steam: its keys are a Steam
# Controller's buttons (desktop layout).
"fromSteam": bool(os.environ.get("SteamGameId")),
"iconUrl": QUrl.fromLocalFile(os.path.realpath(icon)).toString()})
engine.load(os.path.join(HERE, "qml", "Main.qml"))
if not engine.rootObjects():
return 1
app.aboutToQuit.connect(backend.shutdown)
backend.refresh()
return app.exec()
if __name__ == "__main__":
sys.exit(main())