mirror of
https://github.com/theupriser/steamify-cachyos.git
synced 2026-10-03 17:41:58 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceeb949e6b | ||
|
|
03a8b8c4ba | ||
|
|
e699a3f8eb | ||
|
|
7bef834b16 | ||
|
|
86b9744227 | ||
|
|
b0947d7a2f | ||
|
|
3f97c85c3c | ||
|
|
fd162c1e2e | ||
|
|
54ebddfe98 | ||
|
|
fe93dd3e67 | ||
|
|
ef585c0db1 | ||
|
|
32795bd5a5 | ||
|
|
3c8ca65902 | ||
|
|
11a91ef85c | ||
|
|
163f7908c4 | ||
|
|
e121b06fd8 | ||
|
|
19ca966913 | ||
|
|
56fb9080a7 | ||
|
|
6df19eba3e | ||
|
|
7694c97d68 | ||
|
|
a5e644d71a | ||
|
|
929ecfb349 | ||
|
|
4cc87aaea6 | ||
|
|
36d0a9d780 | ||
|
|
cb6361b49b | ||
|
|
c510f9e2e5 | ||
|
|
05a7d50c3f | ||
|
|
badfd846d8 | ||
|
|
ff364ffd69 | ||
|
|
2b3f1de75a | ||
|
|
6d7d5861d6 | ||
|
|
444ce29e57 | ||
|
|
23ef5ef7ab | ||
|
|
178ee3405e | ||
|
|
601c05329d | ||
|
|
8ae9673eb2 | ||
|
|
dbafc43f2e | ||
|
|
ae8ed85fee | ||
|
|
00bcaea712 | ||
|
|
a898d23b13 | ||
|
|
3e53257fe2 | ||
|
|
a1eab9ac79 | ||
|
|
c9c16b9eec | ||
|
|
b17addd60f | ||
|
|
ac613f05b3 | ||
|
|
3a0eefab11 | ||
|
|
96558a9ca8 | ||
|
|
7c48b3d806 | ||
|
|
396459b511 | ||
|
|
43a9541104 | ||
|
|
e09fe1609b | ||
|
|
9dcac5cf08 | ||
|
|
f91dd1f0b1 | ||
|
|
0411429654 | ||
|
|
1aa5e955fe | ||
|
|
ce75e895c4 | ||
|
|
9aef0a986a | ||
|
|
f7aa2412df | ||
|
|
87162efaa2 | ||
|
|
c79a3bbda5 | ||
|
|
ba11f58d9e | ||
|
|
45ff036f09 | ||
|
|
de02f267f4 | ||
|
|
0f123004d0 | ||
|
|
3048b35dd8 | ||
|
|
f7837f82b9 | ||
|
|
3eb41eef5a | ||
|
|
41422e5bb4 | ||
|
|
6c2e203563 | ||
|
|
1522e5632d | ||
|
|
1d994ae359 | ||
|
|
700f3e514b | ||
|
|
d78652c862 | ||
|
|
727e6a2302 | ||
|
|
c4bd1ca39c | ||
|
|
a449a9e467 | ||
|
|
388b1286c9 | ||
|
|
508f22acaf | ||
|
|
3f8e0372d1 | ||
|
|
bd90e29e7a | ||
|
|
4895de3a46 | ||
|
|
0f503dd112 | ||
|
|
d7cf0c0491 | ||
|
|
5ab83d6e6f | ||
|
|
2e107e537e | ||
|
|
986b3c8c07 | ||
|
|
afc05b0b23 | ||
|
|
b0553aecf7 | ||
|
|
69a420b8aa | ||
|
|
1c34a97598 | ||
|
|
be874b8594 | ||
|
|
f01aa93d0e | ||
|
|
852205317a | ||
|
|
77b94c29a2 | ||
|
|
c0014ee623 | ||
|
|
d39731a8b9 | ||
|
|
99a95abf1f | ||
|
|
429e840a9f | ||
|
|
45a8757f58 | ||
|
|
54fc9e362b | ||
|
|
b70c9d614f | ||
|
|
40c9ee638b | ||
|
|
2f9b2a54e1 |
No files matched your search
+24
-7
@@ -1,16 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Build a single-file version of the wizard for `curl | bash` use:
|
||||
# .github/tools/bundle.sh [output] (default: dist/setup-gamescope-boot.sh)
|
||||
# .github/tools/bundle.sh [output] (default: dist/steamify.sh)
|
||||
#
|
||||
# Inlines lib/*.sh in the order setup-gamescope-boot.sh sources them and
|
||||
# Inlines lib/*.sh in the order steamify.sh sources them and
|
||||
# wraps the entry point's body in main(), so bash has read the whole file
|
||||
# before anything runs. That lets the bundle reattach stdin to the terminal
|
||||
# when it was piped in by curl, so the interactive menu still works.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
out="${1:-dist/setup-gamescope-boot.sh}"
|
||||
entry=setup-gamescope-boot.sh
|
||||
out="${1:-dist/steamify.sh}"
|
||||
entry=steamify.sh
|
||||
mkdir -p "$(dirname "$out")"
|
||||
|
||||
# Library order, taken from the entry point's source loop.
|
||||
@@ -23,15 +23,32 @@ version="v$(grep -oP '^VERSION=\K.*' "$entry"), $(git describe --always --dirty
|
||||
# Header comment and `set` line of the entry point, up to the source loop.
|
||||
sed -n '1,/^SCRIPT_DIR=/p' "$entry" | sed '$d'
|
||||
echo "# Single-file build ($version), generated by .github/tools/bundle.sh - edit the"
|
||||
echo "# sources in the repository instead: https://github.com/theupriser/cachyos-gamescope-boot"
|
||||
echo "# sources in the repository instead: https://github.com/theupriser/steamify-cachyos"
|
||||
echo
|
||||
for lib in $libs; do
|
||||
echo "# ----- lib/$lib.sh -----"
|
||||
# Drop each module's shebang and its "Sourced by ..." boilerplate.
|
||||
sed -e '1{/^#!/d}' -e '/^# Sourced by setup-gamescope-boot.sh/d' "lib/$lib.sh"
|
||||
sed -e '1{/^#!/d}' -e '/^# Sourced by steamify.sh/d' "lib/$lib.sh"
|
||||
echo
|
||||
done
|
||||
echo "# ----- setup-gamescope-boot.sh -----"
|
||||
# patches/ embedded: replaces lib/common.sh's patch_file, which reads
|
||||
# them from the checkout.
|
||||
echo "# ----- patches/ -----"
|
||||
echo "patch_file() {"
|
||||
echo ' case "$1" in'
|
||||
for f in patches/*; do
|
||||
[[ "$f" == patches/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
|
||||
echo "# ----- steamify.sh -----"
|
||||
echo "main() {"
|
||||
# Everything after the source loop is the entry point's body.
|
||||
sed -n '/^for lib in /,$p' "$entry" | sed '1,/^done$/d' | sed 's/^/ /; s/^ $//'
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
# Builds the single-file wizard (.github/tools/bundle.sh) and publishes it as a
|
||||
# release per version: tag v$VERSION (VERSION in setup-gamescope-boot.sh),
|
||||
# release per version: tag v$VERSION (VERSION in steamify.sh),
|
||||
# with that version's CHANGELOG.md section as notes. A released version is
|
||||
# never overwritten; the newest one is marked latest, and the "latest" tag and
|
||||
# release are moved to it, so both of these always get the newest version:
|
||||
# curl -fsSL https://github.com/theupriser/cachyos-gamescope-boot/releases/latest/download/setup-gamescope-boot.sh | bash
|
||||
# curl -fsSL https://github.com/theupriser/cachyos-gamescope-boot/releases/download/latest/setup-gamescope-boot.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
|
||||
# Each release also has the app (v2): steamify-app.tar.gz (ui/, steamify.sh,
|
||||
# lib/, patches/, the icon) and its launcher, run with:
|
||||
# curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify-app.sh | bash
|
||||
# A push to feat/v2-ui (the app, not released yet) updates the pre-release
|
||||
# v2-ui-preview instead, whose launcher gets that build:
|
||||
# curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/download/v2-ui-preview/steamify-app.sh | bash
|
||||
# Pull requests only build and check it, and warn when VERSION is already released.
|
||||
name: Single-file build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, feat/v2-ui]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -26,29 +32,39 @@ jobs:
|
||||
fetch-depth: 0 # for the version in the bundle's header
|
||||
|
||||
- name: Build
|
||||
run: .github/tools/bundle.sh dist/setup-gamescope-boot.sh
|
||||
run: |
|
||||
.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 ui assets/steam-gaming-settings.svg LICENSE.md
|
||||
cp ui/steamify-app.sh dist/steamify-app.sh
|
||||
|
||||
- name: Check
|
||||
run: |
|
||||
bash -n dist/setup-gamescope-boot.sh
|
||||
bash -n dist/steamify.sh
|
||||
# SC2034/SC2154: variables used across the inlined modules.
|
||||
shellcheck -S warning -e SC2034,SC2154 dist/setup-gamescope-boot.sh
|
||||
shellcheck -S warning -e SC2034,SC2154 dist/steamify.sh
|
||||
shellcheck -S warning dist/steamify-app.sh
|
||||
python3 -m py_compile ui/steamify-ui
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: setup-gamescope-boot
|
||||
path: dist/setup-gamescope-boot.sh
|
||||
name: steamify
|
||||
path: |
|
||||
dist/steamify.sh
|
||||
dist/steamify-app.tar.gz
|
||||
dist/steamify-app.sh
|
||||
|
||||
- name: Version
|
||||
id: version
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
version="$(grep -oP '^VERSION=\K.*' setup-gamescope-boot.sh)"
|
||||
version="$(grep -oP '^VERSION=\K.*' steamify.sh)"
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
if gh release view "v$version" >/dev/null 2>&1; then
|
||||
echo "released=true" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::v$version is already released and won't be overwritten. Bump VERSION in setup-gamescope-boot.sh and add a CHANGELOG.md section to release this."
|
||||
echo "::warning::v$version is already released and won't be overwritten. Bump VERSION in steamify.sh and add a CHANGELOG.md section to release this."
|
||||
else
|
||||
echo "released=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
@@ -68,12 +84,16 @@ jobs:
|
||||
notes="Single-file build of v$VERSION ($(git describe --always), $(git log -1 --format=%cs)).
|
||||
Run the newest version with:
|
||||
|
||||
curl -fsSL https://github.com/${{ github.repository }}/releases/latest/download/setup-gamescope-boot.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"
|
||||
gh release create "v$VERSION" dist/setup-gamescope-boot.sh \
|
||||
--target "$GITHUB_SHA" --title "v$VERSION" --notes "$notes" --latest
|
||||
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
|
||||
|
||||
# "latest" follows the newest version tag, for the older install URL
|
||||
# (.../releases/download/latest/...). Only moved when a new version
|
||||
@@ -81,8 +101,28 @@ jobs:
|
||||
git tag -f latest "$GITHUB_SHA" && git push -f origin latest
|
||||
latest_notes="Always the newest version, currently v$VERSION (see its release for the changes)."
|
||||
if gh release view latest >/dev/null 2>&1; then
|
||||
gh release edit latest --title "Latest (v$VERSION)" --notes "$latest_notes" --latest=false
|
||||
gh release edit latest --title "Steamify CachyOS - latest (v$VERSION)" --notes "$latest_notes" --latest=false
|
||||
else
|
||||
gh release create latest --title "Latest (v$VERSION)" --notes "$latest_notes" --latest=false
|
||||
gh release create latest --title "Steamify CachyOS - latest (v$VERSION)" --notes "$latest_notes" --latest=false
|
||||
fi
|
||||
gh release upload latest dist/setup-gamescope-boot.sh --clobber
|
||||
gh release upload latest dist/steamify.sh dist/steamify-app.sh dist/steamify-app.tar.gz assets/steam-gaming-settings.svg --clobber
|
||||
|
||||
- name: Publish the v2-ui-preview pre-release
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/feat/v2-ui'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Its launcher downloads this pre-release, not the newest release.
|
||||
sed -i 's/STEAMIFY_RELEASE:-latest/STEAMIFY_RELEASE:-v2-ui-preview/' dist/steamify-app.sh
|
||||
grep -q 'STEAMIFY_RELEASE:-v2-ui-preview' dist/steamify-app.sh
|
||||
git tag -f v2-ui-preview "$GITHUB_SHA" && git push -f origin v2-ui-preview
|
||||
notes="Preview of the Steamify app (v2) from feat/v2-ui ($(git describe --always), $(git log -1 --format='%cs %s')). Not a release.
|
||||
Run it with:
|
||||
|
||||
curl -fsSL https://github.com/${{ github.repository }}/releases/download/v2-ui-preview/steamify-app.sh | bash"
|
||||
if gh release view v2-ui-preview >/dev/null 2>&1; then
|
||||
gh release edit v2-ui-preview --title "Steamify app (v2) preview" --notes "$notes" --prerelease --latest=false
|
||||
else
|
||||
gh release create v2-ui-preview --target "$GITHUB_SHA" --title "Steamify app (v2) preview" --notes "$notes" --prerelease --latest=false
|
||||
fi
|
||||
gh release upload v2-ui-preview dist/steamify-app.sh dist/steamify-app.tar.gz --clobber
|
||||
@@ -1 +1,2 @@
|
||||
dist/
|
||||
__pycache__/
|
||||
@@ -5,19 +5,30 @@ for what the script does from a user's point of view.
|
||||
|
||||
## Project overview
|
||||
|
||||
A bash wizard that makes CachyOS (KDE Plasma 6 + `plasma-login-manager`)
|
||||
**Steamify CachyOS**: a bash wizard that makes CachyOS (KDE Plasma 6 + `plasma-login-manager`)
|
||||
boot into a SteamOS-style gamescope session, with working switching between
|
||||
gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
||||
(DMI `Valve`/`Fremont`) running CachyOS Desktop edition.
|
||||
|
||||
## Layout
|
||||
|
||||
- `setup-gamescope-boot.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
|
||||
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.
|
||||
- `ui/` - the app: `steamify-ui` (PySide6; runs `steamify.sh --backend`,
|
||||
`lib/backend.sh`) and `ui/qml/`: `Main.qml` (window, header, which screen
|
||||
shows), `AppState.qml` (all state and logic, input actions), one
|
||||
`*Screen.qml` per screen, small widgets (`Btn`, `Chip`, `Badge`, ...), and
|
||||
the singletons `Theme` (colours, fonts), `Texts` (item texts) and `Input`
|
||||
(controller/keyboard/remote and its button names), listed in `qmldir`.
|
||||
Screens get the `AppState` as `app` and only show it or call its functions.
|
||||
- `lib/*.sh` - one file per responsibility, each defining functions only
|
||||
(no top-level side effects besides constants). See the table in
|
||||
`README.md`.
|
||||
`TECHNICAL.md`.
|
||||
- **Components.** Every menu item `<id>` (listed in `COMPONENTS` and `LABEL`
|
||||
in `lib/menu.sh`) provides `<id>_status` (return 0 if on, detected from
|
||||
the system, no state file needed), `<id>_enable` and `<id>_disable`, all
|
||||
@@ -25,6 +36,14 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
||||
`<id>_available` is checked via `component_available` (e.g. `machine`
|
||||
only on Fremont). Turn-on order is `COMPONENTS` order, turn-off reverse;
|
||||
`gaming` must stay first. Dependencies live in `toggle_component`.
|
||||
- **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").
|
||||
Status still comes from the system; don't add ad-hoc `<id>_repair` checks
|
||||
for new changes.
|
||||
- **Reversibility.** Every per-user KDE setting a component changes goes
|
||||
through `kset <component> <file> <group|group> <key> <value>`
|
||||
(`lib/state.sh`), which records the old value once; `<id>_disable` calls
|
||||
@@ -59,7 +78,7 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
||||
- Comments explain *why* (the CachyOS/Plasma quirk being worked around),
|
||||
not what the next line does.
|
||||
|
||||
- **Versioning and releases.** SemVer in `VERSION` (`setup-gamescope-boot.sh`,
|
||||
- **Versioning and releases.** SemVer in `VERSION` (`steamify.sh`,
|
||||
shown in the menu header and the bundle header).
|
||||
- Every commit gets an entry in `CHANGELOG.md` under its version (short hash
|
||||
+ subject; a commit can't contain its own hash, so fill it in with the
|
||||
@@ -70,7 +89,7 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
||||
changelog section) and marks it latest. An existing version is never
|
||||
overwritten: without a bump nothing is released, and pull requests show
|
||||
a warning.
|
||||
- Users install through `releases/latest/download/setup-gamescope-boot.sh`
|
||||
- Users install through `releases/latest/download/steamify.sh`
|
||||
(GitHub's newest release). The `latest` tag and release follow the newest
|
||||
version tag too (moved, asset replaced, when a new version is released),
|
||||
so the older URL `releases/download/latest/...` keeps working.
|
||||
@@ -132,16 +151,101 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
||||
and `ensure-kernel-headers.service` installs missing ones at boot (a
|
||||
pacman hook can't run pacman), which triggers DKMS's install hook. The module creates
|
||||
`/sys/class/leds/valve-leds*`.
|
||||
- `boot` ("Boot into: [gamescope] / desktop") is a choice row, not a
|
||||
checkbox: on = desktop. It's a sub-option of the conversion: `menu_visible`
|
||||
only shows it (indented, `└`) while `gaming` is ticked, so menu numbers
|
||||
after it shift by one when the conversion is unticked. It needs `gaming` (ticking it ticks gaming,
|
||||
unticking gaming unticks it) and is never preselected (`NO_PRESELECT`), so
|
||||
the conversion boots into gamescope by default. Desktop is
|
||||
`steamify-boot-desktop.service`, ordered `Before=` the login managers:
|
||||
`steam-set-session plasma.desktop` plus the plasmalogin sync bridge when it
|
||||
exists, at every boot. CachyOS's `cachyos-gamescope-autologin` still sets
|
||||
gamescope during each desktop session; the unit corrects it at boot.
|
||||
- `bios` is an *action* (`ACTIONS` in `lib/menu.sh`) and a sub-option of `machine`, not an on/off
|
||||
component: never preselected (not even on a first run), never re-applied
|
||||
by `a`, not listed in the state overview, and `bios_status` is always off.
|
||||
Only selectable when Valve's version differs from the installed one
|
||||
(`component_selectable`, greyed out otherwise). Download, SHA-256 and the
|
||||
fwupd device check (`get-details --json`: no `UpdateError`) come before the
|
||||
warnings. The warning box uses `█` for its frame: Konsole draws a long
|
||||
coloured row of `#` narrower, so the right edge wouldn't line up.
|
||||
Keep both confirmations (y/N, then typing `UPDATE`) and the warnings; the
|
||||
firmware comes from the newest `holo-X.Y` repo (`.files` db names the
|
||||
`.cab`, `.db` gives the SHA-256), and fwupd itself refuses non-Fremont
|
||||
hardware. In the VM, test it with `WIZARD_BIOS_DRY_RUN=1` (skips only the
|
||||
device check, never flashes) and a faked version (dev-env
|
||||
`BIOS_VERSION=F7F0107 ./run.sh --fremont`).
|
||||
- The entry point loops: menu, run, "back to the menu" (or `[m]`/`[r]` when a
|
||||
restart is needed), until `q`. When a restart is needed, `q` asks "Restart
|
||||
now? [Y/n]" (`quit_prompt`): `n` goes back to the menu, the next `q` asks
|
||||
again. Scripted input that runs out quits without restarting (never
|
||||
restart on EOF: `ask_yn` would take its default).
|
||||
- Steamify shortcut (`launcher`): "Steamify CachyOS" (desktop and launcher,
|
||||
id `steamify-ui.desktop`, the app's own id) runs `curl | bash` of
|
||||
`releases/latest/download/steamify-app.sh` without a terminal (in Konsole
|
||||
only while PySide6 is missing, for sudo); "Steamify Terminal" (launcher)
|
||||
runs `steamify.sh` in Konsole. Both always the newest release. The entry
|
||||
has `X-Steamify-Shortcut=true`: steamify-app.sh doesn't overwrite it, and
|
||||
disable only removes it then. A pre-2.0.1 shortcut (terminal only) is
|
||||
ticked by `launcher_repair`. Its icon, `assets/steam-gaming-settings.svg`
|
||||
(Valve's GPL-2.0 return icon with a gear), is a release asset too, and is
|
||||
downloaded from there (Steam's icon if that fails). Desktop files are
|
||||
written with their mode already set (`install_executable`): Plasma opens a
|
||||
desktop icon it first saw non-executable in an editor.
|
||||
The start script closes its window after a 10-second countdown on success
|
||||
and waits for Enter after an error; it uses `pipefail`, or a failed
|
||||
download would run an empty script and count as success.
|
||||
- HDMI refresh boost (`hdmi`, `lib/hdmi-refresh.sh`): retired in 2.2.0
|
||||
(it needed the pinned kernel); only removal is left (see the power-off
|
||||
fix below). `hdmi_enable` refuses. debugfs is root-only (glob it under
|
||||
sudo), and `edid_override` takes exactly `reset` with no newline.
|
||||
- Steam Machine CEC driver (`cec_driver_enable`, `lib/cec.sh`): mainline
|
||||
`cros_ec_cec` lacks Fremont, so HDMI-CEC builds Valve's copy (evlaV
|
||||
`linux-integration`, pinned commit + SHA-256) with DKMS for every kernel.
|
||||
It's patched to register its notifier without a port name when the board
|
||||
has one CEC port: amdgpu registers its HDMI notifier nameless, and the
|
||||
kernel only pairs that with a named lookup ("Port C") when the CEC driver
|
||||
registered first, which never happens since amdgpu loads from the
|
||||
initramfs. Without it `/dev/cec0` exists but stays at `f.f.f.f`.
|
||||
- Power-off fix (`poweroff`, `lib/fremont-poweroff.sh`, a default sub-option
|
||||
of `machine`): recent kernels (7.2 and the 6.x/7.0/7.1 updates with the
|
||||
backport) keep the firmware's S4/S5 wake bit on GPIO
|
||||
pin 18, so the Steam Machine boots again right after powering off
|
||||
(Valve's kernel clears it at probe, not for upstream). The DKMS module
|
||||
`steamify-fremont-poweroff` (`patches/steamify-fremont-poweroff.c`) clears
|
||||
it in a power-off-prepare handler, built for each kernel. It made the kernel pin
|
||||
unnecessary: `kpin` is only available while on (`kpin_available`), and
|
||||
`detect_components` unticks it, so a normal run removes an existing pin.
|
||||
`hdmi` (it needed the pin) is retired the same way: `hdmi_status` is on
|
||||
while anything of it is left (saved displays, hotplug unit, EDID files,
|
||||
old kernel parameter), `hdmi_available` only then, it's always unticked,
|
||||
and `hdmi_disable` removes all of it. CachyOS's
|
||||
`linux-cachyos` is clang-built, `-bore` GCC-built: let DKMS pick the
|
||||
compiler, never pass `LLVM=1`. Test shutdown on the real machine for every
|
||||
new major kernel.
|
||||
- VRAM booster (`vram`, `lib/vram-booster.sh`, top-level, ticked by
|
||||
default, 2.3.0; only available when `/sys/fs/cgroup/dmem.capacity` lists a
|
||||
VRAM region of at least 2 GB; greyed out with an NVIDIA card,
|
||||
`vram_selectable`, faked with `WIZARD_VRAM_FAKE_NVIDIA=1`): CachyOS's `dmemcg-booster` (system + user service) and
|
||||
`plasma-foreground-booster`, like SteamOS 3.9's VRAM management. The
|
||||
latter only starts with `kcgroupsrc [Foreground Booster] autostart=true`
|
||||
(set with `kset`). Disable removes only the packages it installed
|
||||
(`state_get vram installed_pkgs`).
|
||||
- `Relogin=true` means a gamescope that fails to start is relaunched in a
|
||||
tight loop; keep that in mind when changing session handling.
|
||||
|
||||
## Checking changes
|
||||
|
||||
The bundle puts every module in one file, so shellcheck sees all their
|
||||
`local` variables together: don't reuse a name another module uses as an
|
||||
array (e.g. `g` in `lib/state.sh`), or CI's shellcheck on the bundle fails.
|
||||
|
||||
|
||||
There is no test suite. At minimum:
|
||||
|
||||
```bash
|
||||
for f in setup-gamescope-boot.sh lib/*.sh; do bash -n "$f"; done
|
||||
shellcheck -S warning setup-gamescope-boot.sh lib/*.sh # if available
|
||||
for f in steamify.sh lib/*.sh; do bash -n "$f"; done
|
||||
shellcheck -S warning steamify.sh lib/*.sh # if available
|
||||
```
|
||||
|
||||
Behaviour is verified in a CachyOS QEMU/KVM test VM. The VM scripts and a
|
||||
@@ -150,5 +254,5 @@ the wizard with scripted menu input such as `printf '2\n\ny\nn\n'`, the
|
||||
per-component checks; when stdin is not a terminal the menu falls back to a
|
||||
numbered prompt, which is what scripted runs use, reboot checks, the full test matrix, `--fremont` to
|
||||
fake Steam Machine hardware) live in
|
||||
[cachyos-gamescope-boot-dev-env](https://github.com/theupriser/cachyos-gamescope-boot-dev-env).
|
||||
[steamify-cachyos-dev](https://github.com/theupriser/steamify-cachyos-dev).
|
||||
Gamescope itself and the real LED bar can only be verified on hardware.
|
||||
@@ -0,0 +1,48 @@
|
||||
# HDMI-CEC: does my PC have it?
|
||||
|
||||
The **HDMI-CEC** item in Steamify lets you use Steam with your TV's remote,
|
||||
and turns the TV on and off with the PC. It only works when the PC has a
|
||||
CEC device (`/dev/cec*`). Many PCs don't: on desktop PCs CEC is rare.
|
||||
How it works: [TECHNICAL.md](TECHNICAL.md#hdmi-cec); problems:
|
||||
[TROUBLESHOOTING.md](TROUBLESHOOTING.md#the-tv-remote-does-nothing-hdmi-cec).
|
||||
|
||||
## Graphics cards
|
||||
|
||||
- **NVIDIA:** no. The driver doesn't expose CEC on the HDMI port, on Linux
|
||||
or Windows.
|
||||
- **AMD and Intel:** the CEC line of the HDMI port is normally not
|
||||
connected on desktop cards. The Linux drivers (`amdgpu`, `i915`) only
|
||||
support CEC over a **DisplayPort** connection: some DisplayPort-to-HDMI
|
||||
adapters and built-in converter chips pass CEC along ("DP-AUX CEC"). That
|
||||
depends on the adapter or chip, not on the graphics card, so it's worth
|
||||
trying a different adapter.
|
||||
|
||||
## Devices with CEC built in
|
||||
|
||||
- the **Valve Steam Machine** (Steamify ticks HDMI-CEC by default there);
|
||||
- some **Intel NUC** mini PCs, which have a CEC chip on the board;
|
||||
- the **Raspberry Pi**.
|
||||
|
||||
## Any PC: a USB CEC adapter
|
||||
|
||||
A USB CEC adapter, such as the **Pulse-Eight USB-CEC adapter** (or a
|
||||
RainShadow Tech one), sits between the PC and the TV on the HDMI cable and
|
||||
works on Linux. This is the way to get CEC on a custom build, e.g. with an
|
||||
NVIDIA card. Steamify installs what these adapters need
|
||||
(`inputattach-cec-units`), so plug it in and turn on HDMI-CEC.
|
||||
|
||||
## Check your PC
|
||||
|
||||
After turning on HDMI-CEC in Steamify, the menu lists the CEC devices it
|
||||
found. Or run:
|
||||
|
||||
```bash
|
||||
ls /dev/cec*
|
||||
```
|
||||
|
||||
No device (and no USB adapter): no CEC on this PC. Also turn on CEC on the
|
||||
TV itself: it has a brand name there (Sony: BRAVIA Sync, Samsung: Anynet+,
|
||||
LG: SimpLink, Philips: EasyLink, Panasonic: VIERA Link).
|
||||
|
||||
This overview is based on how the Linux drivers work; hardware differs,
|
||||
so the only sure test is `ls /dev/cec*` on your own PC.
|
||||
+328
-3
@@ -2,10 +2,335 @@
|
||||
|
||||
All notable changes, per version and per commit. Versions follow
|
||||
[Semantic Versioning](https://semver.org/); the current one is `VERSION` in
|
||||
`setup-gamescope-boot.sh`. Versions before 0.7.0 were numbered afterwards,
|
||||
`steamify.sh`. Versions before 0.7.0 were numbered afterwards,
|
||||
one per merged pull request.
|
||||
|
||||
## 0.7.0 - 2026-09-24
|
||||
## 2.4.0 - 2026-09-27
|
||||
|
||||
- **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
|
||||
|
||||
- **fix: Steam's HDMI-CEC settings reach cecd on a first run**
|
||||
- HDMI-CEC runs before Steam Machine support, which installs
|
||||
steamos-manager; so steamos-manager's `configure-cecd` unit couldn't be
|
||||
enabled and cecd ran without Steam's settings. Steam Machine support now
|
||||
links them once steamos-manager is there.
|
||||
|
||||
## 1.1.1 - 2026-09-25
|
||||
|
||||
- **perf: Pinned kernel before the LED driver**
|
||||
- With the kernel pin ticked, Steam Machine support installs kernel
|
||||
7.1.6-1 first, so DKMS builds the LED driver once, for that kernel,
|
||||
instead of for the current kernel first and then again. The LED driver
|
||||
then loads after the restart, which it now says instead of warning.
|
||||
- **fix: steamos-manager starts after cecd**
|
||||
- It checks only at its start whether cecd runs; if it was first, Steam's
|
||||
HDMI-CEC settings were missing until it restarted. A drop-in
|
||||
(`/etc/systemd/user/steamos-manager.service.d/`) orders it after cecd.
|
||||
|
||||
## 1.1.0 - 2026-09-25
|
||||
|
||||
- **feat: HDMI-CEC** (experimental, on every PC; ticked by default only on a
|
||||
Steam Machine)
|
||||
- Valve's `cecd`, `cec-audio-control` and `inputattach-cec-units` from its
|
||||
SteamOS repository (SHA-256 checked): use Steam with the TV remote, and
|
||||
the TV turns on and off with the PC. On a Steam Machine, steamos-manager
|
||||
configures it from Steam's settings.
|
||||
- Works with any `/dev/cec*` (GPU or USB adapter); the menu lists them.
|
||||
- `CEC.md`: which PCs have CEC (NVIDIA doesn't, AMD/Intel only over some
|
||||
DisplayPort adapters), and USB adapters for your own build.
|
||||
- Restarts steamos-manager, which only offers Steam its HDMI-CEC settings
|
||||
when cecd was there at its start.
|
||||
- The README no longer says Steam Machine support alone gives HDMI-CEC:
|
||||
`cecd` wasn't installed, so it didn't.
|
||||
|
||||
## 1.0.0 - 2026-09-25
|
||||
|
||||
- **feat: Pin the kernel to 7.1.6-1 on a Steam Machine**
|
||||
- New sub-option under Steam Machine support, ticked along with it (opt
|
||||
out): newer CachyOS kernels make the Steam Machine reboot instead of
|
||||
shutting down. Installs `linux-cachyos` and `-headers` 7.1.6-1 and adds
|
||||
them to `IgnorePkg`; unticking removes the pin and updates the system.
|
||||
- The packages are kept in `/var/cache/steamify/kernel` and reinstalled
|
||||
from there without downloading; else from pacman's cache, the CachyOS
|
||||
archive, the CachyOS mirror, or `PINNED_KERNEL_URL`.
|
||||
- First download source: the `kernel-7.1.6-1` release of this repo, a
|
||||
one-off release that always has these files.
|
||||
- Every file is checked against its SHA-256 (in the script) and its
|
||||
CachyOS signature before it's installed; a bad file is deleted.
|
||||
- Installed kernel headers are no longer updated by the wizard, so the
|
||||
pinned kernel keeps matching headers.
|
||||
- The BIOS item moves down one (9 on a Steam Machine).
|
||||
- **docs: Shorter README, with a Steam Machine section**
|
||||
- How it works moved to `TECHNICAL.md`, problems to `TROUBLESHOOTING.md`;
|
||||
the README links to them and to the files it mentions.
|
||||
- **feat: No more KDE wallet password prompts in single user mode**
|
||||
- Uses Valve's empty, password-less wallet, like SteamOS; apps such as
|
||||
Brave no longer ask for the wallet password after autologin.
|
||||
- Your own wallet is moved to `kdewallet.kwl.bak-steamify` and comes back,
|
||||
unchanged, when single user mode is off; the single user wallet is kept
|
||||
and reused next time.
|
||||
- Moved from the SteamOS theme, where it only helped without any wallet.
|
||||
- **fix: No second sudo password prompt while installing yay**
|
||||
- `makepkg -si` installs with `sudo -k`, which forgets the cached password;
|
||||
yay is now built with makepkg and installed with `sudo pacman -U`.
|
||||
- **docs: LICENSE.md** with the MIT license the README already named.
|
||||
- **chore: No more `setup-gamescope-boot.sh` release asset**
|
||||
- Only `steamify.sh` is published; the old name from before 0.9.0 is gone.
|
||||
|
||||
## 0.10.0 - 2026-09-24
|
||||
|
||||
- `0f503dd` **feat: Boot into gamescope or the desktop**
|
||||
- New menu row under the conversion, "Boot into: [gamescope] / desktop";
|
||||
gamescope stays the default and is never changed on a first run.
|
||||
- Desktop: `steamify-boot-desktop.service` sets the autologin session to
|
||||
Plasma at every boot, before the login manager starts; Return to Gaming
|
||||
Mode and Steam's Switch to Desktop keep working. Back to gamescope removes
|
||||
it. Choosing desktop turns the conversion on; turning the conversion off
|
||||
resets it to gamescope.
|
||||
- The later menu items move down one (BIOS is now 8 on a Steam Machine).
|
||||
- `4895de3` **feat: "Boot into" is a sub-option of the conversion**
|
||||
- Shown indented under the SteamOS conversion, and only while it's ticked.
|
||||
- `bd90e29` **feat: Left/Right arrows choose on the "Boot into" row**
|
||||
- Left = gamescope, right = desktop (Space still switches); the row says
|
||||
"←/→ choose" and the key help lists Left/Right.
|
||||
- **fix: Messages say which way it boots**
|
||||
- "This will: boot into: gamescope (from the next boot)" instead of "turn
|
||||
off: Boot into the desktop instead of gaming mode", and the same in the
|
||||
progress line and the "Done" overview.
|
||||
|
||||
## 0.9.1 - 2026-09-24 (#10)
|
||||
|
||||
- `2e107e5` **fix: LED driver for every kernel on a fresh install**
|
||||
- The DKMS override was written to `/etc/dkms` before `dkms` was installed,
|
||||
when that folder doesn't exist yet, so it silently failed and the build
|
||||
for other kernels (e.g. LTS) failed as in 0.6. The folder is created first
|
||||
now, and a failed write stops with an error.
|
||||
- `5ab83d6` **feat: Restart question on quit is "Restart now? [Y/n]"**
|
||||
- When something needs a restart, `q` asks with yes as the default; `n` goes
|
||||
back to the menu and the next `q` asks again. Ctrl+C quits without
|
||||
restarting; scripted input that runs out never restarts.
|
||||
|
||||
## 0.9.0 - 2026-09-24 (#9)
|
||||
|
||||
The wizard is now called **Steamify CachyOS**, and can put a shortcut to
|
||||
itself on the desktop.
|
||||
|
||||
- `f01aa93` **feat: Wizard shortcut on the desktop and in the launcher**
|
||||
- New menu item: a desktop icon and app launcher entry
|
||||
that open the newest release of the wizard in Konsole (`curl | bash`).
|
||||
- Its icon: Valve's Return to Gaming Mode icon with a gear instead of the
|
||||
arrow (`assets/steam-gaming-settings.svg`), published with every release
|
||||
and downloaded from there.
|
||||
- Desktop files are written already executable, so Plasma runs them instead
|
||||
of opening them in an editor.
|
||||
- `be874b8` **feat: Rename to Steamify CachyOS**
|
||||
- Menu header, the shortcut ("Steamify CachyOS", menu item "Steamify
|
||||
shortcut"), release titles, README and AGENTS.md.
|
||||
- `1c34a97` **chore: Repository renamed to steamify-cachyos**
|
||||
- All URLs point to `github.com/theupriser/steamify-cachyos` (the old ones
|
||||
redirect). The script keeps its name, `setup-gamescope-boot.sh`, so the
|
||||
old download URL keeps working; internal names such as
|
||||
`~/.local/state/cachyos-gamescope-boot` stay, so existing installs can
|
||||
still be turned off.
|
||||
- `69a420b` **feat: The script is now steamify.sh**
|
||||
- `setup-gamescope-boot.sh` is renamed to `steamify.sh`; releases publish the
|
||||
bundle under both names, so `.../download/setup-gamescope-boot.sh` keeps
|
||||
working. The Steamify shortcut runs `steamify.sh`.
|
||||
- `b0553ae` **docs: Shorter README intro**
|
||||
- `afc05b0` **feat: The shortcut's window closes itself after a countdown**
|
||||
- After the wizard ends, a 10-second countdown closes the Konsole window
|
||||
(Enter closes it right away); after an error it stays open until Enter.
|
||||
- A failed download now counts as an error (`pipefail`) instead of running
|
||||
an empty script.
|
||||
|
||||
## 0.8.0 - 2026-09-24 (#8)
|
||||
|
||||
An opt-in BIOS update for the Steam Machine, and a menu that comes back after
|
||||
every run.
|
||||
|
||||
- `2f9b2a5` **feat: Opt-in BIOS update for the Steam Machine**
|
||||
- New menu item (Steam Machine only, never ticked by default) showing the
|
||||
current BIOS version and the newest from Valve's `fremont-hw-support`.
|
||||
- Two large warnings and two confirmations (y/N, then typing `UPDATE`),
|
||||
checksum-verified download, installed with fwupd; the wizard then offers
|
||||
the restart that writes it, with a warning to keep the power on.
|
||||
- `40c9ee6` **fix: BIOS update only when newer, device check first, aligned warnings**
|
||||
- The item is greyed out and can't be ticked unless Valve has a newer BIOS.
|
||||
- Before any warning: SHA-256 of Valve's package, then fwupd confirms the
|
||||
firmware fits this machine's hardware; otherwise it stops.
|
||||
- Warning box drawn with a solid red frame whose edges line up, and the
|
||||
menu's "Now" column aligned.
|
||||
- `b70c9d6` **fix: shellcheck in the bundle, and the docs for the BIOS checks**
|
||||
- A variable name in `lib/bios.sh` clashed with `lib/state.sh` in the bundle
|
||||
(CI's shellcheck would fail); README, AGENTS.md and this changelog updated.
|
||||
- `54fc9e3` **feat: Menu comes back after each run, BIOS dry run, version 0.8.0**
|
||||
- After a run the menu returns with the new state; quit with `q`. When
|
||||
something needs a restart, choose between back to the menu (`m`) and
|
||||
restart now (`r`); quitting asks once more.
|
||||
- A staged BIOS update greys the item out until the restart.
|
||||
- `WIZARD_BIOS_DRY_RUN=1` walks through the whole BIOS update (download,
|
||||
checksum, both warnings) but only prints the install and never flashes.
|
||||
- README: the menu loop and the BIOS update step by step.
|
||||
- `45a8757` **fix: BIOS dry run also walks through the restart choices**
|
||||
- A dry run counts as staged, so `[m]`/`[r]` and the restart question on `q`
|
||||
show up; in dry-run mode restarting only prints what it would do.
|
||||
- `429e840` **fix: Shorter BIOS labels so the menu fits 80 columns**
|
||||
- "now F7F0107, newest F7F0108 (own risk)", "F7F0108 waits for a restart",
|
||||
"F7F0107 is up to date"; shorter dry-run line.
|
||||
- `99a95ab` **fix: Kernel legend only mentions the LEDs on a Steam Machine**
|
||||
- `c0014ee` **docs: Last commit hash in the changelog; 0.7.0 was released on its own (#6, #7)**
|
||||
|
||||
## 0.7.0 - 2026-09-24 (#6, #7)
|
||||
|
||||
The SteamOS theme comes from CachyOS's `cachyos-vapor` package, applied with
|
||||
Vapor's own desktop layout, and gains the SteamOS desktop extras. The Steam
|
||||
@@ -52,7 +377,7 @@ Machine LED driver works on every installed kernel and survives kernel updates.
|
||||
- Install with `.../releases/latest/download/setup-gamescope-boot.sh`, which
|
||||
always points to the newest release.
|
||||
- `1604ed1` **docs: Versioning and release rules in AGENTS.md**
|
||||
- **ci: The latest tag follows the newest version tag**
|
||||
- `6afef78` **ci: The latest tag follows the newest version tag**
|
||||
- When a new version is released, the `latest` tag and release move to it
|
||||
(bundle replaced), so the older `.../releases/download/latest/...` URL
|
||||
also always gives the newest version.
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# MIT License
|
||||
|
||||
Copyright (c) 2026 Rick Peters
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
The kernel packages in the `kernel-7.1.6-1` release are unmodified CachyOS
|
||||
packages under their own license (GPL-2.0); see that release for the source.
|
||||
@@ -1,32 +1,61 @@
|
||||
# CachyOS Gamescope Boot Wizard
|
||||
# Steamify CachyOS
|
||||
|
||||
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
|
||||
desktop and back whenever you like - just like on a Steam Deck or Valve's
|
||||
Steam Machine.
|
||||
|
||||
Out of the box, CachyOS doesn't quite manage this: the switch to the desktop
|
||||
can hang, and the PC doesn't reliably boot back into gaming mode. This
|
||||
script fixes that and sets everything up for you.
|
||||
- **One command**, then a simple checklist: pick what you want.
|
||||
- **Everything is undoable**: untick something and it's put back the way it was.
|
||||
- **Made for the Steam Machine**, and works on any CachyOS KDE desktop PC.
|
||||
|
||||
## What you get
|
||||
|
||||
The wizard is a menu: pick what you want, and it turns each part on or off.
|
||||
Everything you turn off is put back the way it was.
|
||||
|
||||
1. **SteamOS conversion** - boots straight into gaming mode, **Switch to
|
||||
Desktop** in Steam works and so does going back (the **Return to Gaming
|
||||
Mode** icon on the desktop), you're back in gaming mode after a restart,
|
||||
and Steam's on-screen keyboard (Steam + X) also works on the desktop.
|
||||
2. **SteamOS theme** - the Vapor look for the desktop (CachyOS's
|
||||
`cachyos-vapor` package).
|
||||
1. **SteamOS conversion** - boots straight into gaming mode. **Switch to
|
||||
Desktop** in Steam works, and so does going back (the **Return to Gaming
|
||||
Mode** icon on the desktop). Steam's on-screen keyboard (Steam + X) works
|
||||
on the desktop too.
|
||||
- **Boot into: gamescope / desktop** - where the PC starts. Gaming mode is
|
||||
the default.
|
||||
2. **SteamOS theme** - the Vapor look of SteamOS for the desktop, with
|
||||
**Add to Steam** in right-click menus
|
||||
([details](TECHNICAL.md#steamos-desktop-look)).
|
||||
3. **Steam Deck/Machine icons** - Steam Deck button icons in gaming mode.
|
||||
4. **Single user mode** - like SteamOS: never a login or lock screen, no
|
||||
user switching or logging out. Typing a password with a controller is no
|
||||
fun. (Needs 1.)
|
||||
5. **Steam Machine support** - only shown on a Valve Steam Machine: the
|
||||
driver for the front LED bar, and the hardware settings in Steam (fan,
|
||||
TV control over HDMI-CEC).
|
||||
4. **Single user mode** - like SteamOS: never a login or lock screen, and no
|
||||
KDE wallet password prompts (e.g. from Brave). Typing a password with a
|
||||
controller is no fun
|
||||
([details](TECHNICAL.md#single-user-mode-sddm-no-locking)).
|
||||
5. **Steamify shortcut** - "Steamify CachyOS" on the desktop and in the
|
||||
launcher opens the newest Steamify app, so you don't need the install
|
||||
command again. "Steamify Terminal" in the launcher opens the terminal menu.
|
||||
6. **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) on kernel 7.2 or newer; greyed out
|
||||
with an NVIDIA card until NVIDIA's driver supports it
|
||||
([details](TECHNICAL.md#vram-booster)).
|
||||
7. **HDMI-CEC** (experimental; off by default, on by default on a Steam
|
||||
Machine) - use Steam with your TV's
|
||||
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
|
||||
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)).
|
||||
|
||||
## Steam Machine
|
||||
|
||||
What Steamify can do on a Valve Steam Machine:
|
||||
|
||||
- **HDMI-CEC** - ticked by default here, as on SteamOS: use Steam with your
|
||||
TV's remote, and the TV turns on and off with the Steam Machine
|
||||
([details](TECHNICAL.md#hdmi-cec)).
|
||||
- **Steam Machine support** - the front **LED bar** works, Steam's
|
||||
**hardware settings** (fan, performance) work, and the power button puts
|
||||
it to sleep like a console.
|
||||
- **Power-off fix** - on by default: shutting down really turns the Steam
|
||||
Machine off, also with the newest CachyOS kernel
|
||||
([details](TECHNICAL.md#power-off-fix-steam-machine)).
|
||||
- **Update BIOS** - installs Valve's newest Steam Machine BIOS. Never ticked
|
||||
by default, at your own risk, and only after two warnings
|
||||
([details](TECHNICAL.md#bios-updates-steam-machine)).
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -38,324 +67,71 @@ Everything you turn off is put back the way it was.
|
||||
Open **Konsole** on your Plasma desktop and run:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://github.com/theupriser/cachyos-gamescope-boot/releases/latest/download/setup-gamescope-boot.sh | bash
|
||||
curl -fsSL https://github.com/theupriser/steamify-cachyos/releases/latest/download/steamify.sh | bash
|
||||
```
|
||||
|
||||
That downloads and runs the latest single-file version, and works in any
|
||||
shell, including fish (CachyOS's default). Prefer to keep a copy, or to look
|
||||
at the script first? Clone the repository instead:
|
||||
Prefer to look at the script first? Clone the repository and run
|
||||
[`./steamify.sh`](steamify.sh) (keep the whole folder: it needs
|
||||
[`lib/`](lib/)).
|
||||
|
||||
```bash
|
||||
git clone https://github.com/theupriser/cachyos-gamescope-boot.git
|
||||
cd cachyos-gamescope-boot
|
||||
./setup-gamescope-boot.sh
|
||||
```
|
||||
|
||||
Run it as yourself, not as root. You'll see a checklist:
|
||||
You'll see a checklist:
|
||||
|
||||
```
|
||||
[x] SteamOS conversion: boot into gaming mode, Steam on the desktop (now: off)
|
||||
└ Boot into: [gamescope] desktop
|
||||
> [x] Install SteamOS theme: Vapor look (cachyos-vapor) (now: off)
|
||||
[x] Install Steam Deck/Machine icons: Deck button icons in gaming mode (now: off)
|
||||
[x] Single user mode: no password, lock screen or log out (SDDM) (now: off)
|
||||
[x] Steamify shortcut: desktop icon to run this again (now: off)
|
||||
|
||||
Kernels (> = running; controller = Steam controller driver, LEDs = LED bar driver built)
|
||||
6.18.52-1-cachyos-lts linux-cachyos-lts headers yes controller yes
|
||||
> 7.2.7-1-cachyos linux-cachyos headers yes controller yes
|
||||
|
||||
Up/Down move Space select Enter run a run + re-apply what's on q quit
|
||||
Up/Down move Space select Left/Right choose Enter run
|
||||
a run + re-apply what's on q quit
|
||||
```
|
||||
|
||||
Move with the **arrow keys**, tick or untick with **Space**, and press
|
||||
**Enter** to run. "now:" shows what's on at the moment. Below the list, every
|
||||
installed kernel is shown with its headers and Steam controller driver; on a
|
||||
Steam Machine also whether the LED bar driver is built for it, and whether
|
||||
it's loaded right now, so a kernel update is easy to check. The wizard then
|
||||
shows what it will change, asks your password once, and at the end offers
|
||||
to restart (needed for changes to how the PC starts).
|
||||
Tick with **Space**, run with **Enter**. It shows what it will change, asks
|
||||
your password once, and offers to restart at the end. Below the list you see
|
||||
every installed kernel, so after an update it's easy to check that
|
||||
everything is in place.
|
||||
|
||||
Run it again whenever you like - to change your choices, to turn things off
|
||||
again, or after a CachyOS update (press `a` in the menu to re-apply
|
||||
everything that's on).
|
||||
|
||||
If you cloned the repository, keep the whole folder: the script needs the
|
||||
files in `lib/` next to it.
|
||||
Run it again whenever you like: to change your choices, or after a CachyOS
|
||||
update (press `a` to re-apply what's on).
|
||||
|
||||
## Using it
|
||||
|
||||
- **To the desktop:** in gaming mode, open the Steam menu and choose
|
||||
**Power > Switch to Desktop**.
|
||||
- **Back to gaming mode:** double-click **Return to Gaming Mode** on the
|
||||
desktop (or log out, if you're not using single user mode).
|
||||
- After a restart you always start in gaming mode.
|
||||
desktop.
|
||||
- **Remove it all:** run the wizard and untick everything. You get the
|
||||
normal CachyOS login screen back; Steam stays installed.
|
||||
|
||||
Prefer to decide yourself where your PC starts? Run one of these in Konsole:
|
||||
Prefer to start where you left off last time?
|
||||
|
||||
```bash
|
||||
steamos-session-select persistent # start where you left off last time
|
||||
steamos-session-select oneshot # always start in gaming mode (default)
|
||||
```
|
||||
|
||||
## If something goes wrong
|
||||
## Something wrong?
|
||||
|
||||
**Gaming mode shows a black screen or keeps restarting.** Press
|
||||
**Ctrl+Alt+F3**, log in with your username and password, and run:
|
||||
Stuck on a black screen in gaming mode: press **Ctrl+Alt+F3**, log in, and run
|
||||
|
||||
```bash
|
||||
sudo /usr/lib/steamos/steam-set-session plasma.desktop && sudo systemctl restart display-manager
|
||||
```
|
||||
|
||||
That takes you back to the desktop.
|
||||
|
||||
**The LED bar on the Steam Machine stays dark.** Restart the PC once more; a
|
||||
freshly installed driver often needs a reboot. Still dark? See
|
||||
[Front LED bar](#front-led-bar-steam-machine) under technical details.
|
||||
|
||||
**Undo something:** run the wizard again and untick it.
|
||||
|
||||
---
|
||||
|
||||
## Technical details
|
||||
|
||||
### How turning things on and off works
|
||||
|
||||
At startup every component checks whether it is on (`*_status` in `lib/`).
|
||||
You choose; the wizard then turns off what you unticked (in reverse order)
|
||||
and turns on what you ticked. To be able to undo:
|
||||
|
||||
- **System files** are backed up once, next to the original, with a
|
||||
`.bak-gamescope-wizard` suffix, and restored when you turn the component
|
||||
off.
|
||||
- **KDE settings** in your home directory are recorded with their previous
|
||||
value the first time the wizard changes them, in an undo journal under
|
||||
`~/.local/state/cachyos-gamescope-boot/`. Turning the component off writes
|
||||
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
|
||||
KDE's defaults.
|
||||
- **Packages** installed for the conversion (Steam, gamescope-session, ...)
|
||||
are kept when you turn it off; Steam Machine support removes its own.
|
||||
|
||||
### Single user mode: SDDM, no locking
|
||||
|
||||
Turning it on switches to **SDDM** and turns off everything that asks for a
|
||||
password or another user, per user: `action/lock_screen`, `switch_user` and
|
||||
`start_new_session` restrictions in `kdeglobals`, no automatic locking
|
||||
(`kscreenlockerrc`), Meta+L and Ctrl+Alt+Del unbound, and the launcher shows
|
||||
only Sleep / Restart / Shut Down (kickoff `primaryActions=3`), which hides
|
||||
the Session dropdown with Log Out. (Restricting `action/logout` would also
|
||||
hide Restart and Shut Down.) Turning it off restores all of that and moves
|
||||
the conversion back to plasma-login-manager.
|
||||
|
||||
**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
|
||||
honours. The script installs and enables `sddm` (active from the next boot),
|
||||
writes `User=`, `Session=` and `Relogin=true` to
|
||||
`/etc/sddm.conf.d/10-gamescope-autologin.conf`, and removes any
|
||||
`[Autologin]` from `/etc/sddm.conf` (read last, so it would override both).
|
||||
No sync bridge or sudoers rule is needed; the shortcut just runs
|
||||
`steamos-session-select gamescope`, which logs out, and `Relogin=true` logs
|
||||
straight back in to gamescope.
|
||||
|
||||
Without single user mode, the conversion uses **plasma-login-manager**
|
||||
(CachyOS's default since March 2026), which needs the workarounds below.
|
||||
|
||||
### Why this is needed (plasma-login-manager)
|
||||
|
||||
CachyOS's `gamescope-session-cachyos` package ships `steamos-session-select`,
|
||||
which is meant to work exactly like it does on real SteamOS. Since the March
|
||||
2026 release, CachyOS uses `plasma-login-manager` instead of SDDM, and three
|
||||
things get in the way:
|
||||
|
||||
1. **No `User=` written to the autologin config.**
|
||||
`steam-set-session` (called by `steamos-session-select`) only writes a
|
||||
`Session=` key. `plasma-login-manager` needs both `Session=` and `User=`
|
||||
to autologin at all; without `User=` it shows the greeter every boot.
|
||||
|
||||
2. **Missing `/etc/plasmalogin.conf.d` directory.**
|
||||
Without it, `steam-set-session` fails, and Steam's "Switch to Desktop"
|
||||
hangs forever. *(Fixed upstream in `gamescope-session-cachyos` 1.1.6; the
|
||||
script still creates the directory for older versions.)*
|
||||
|
||||
3. **The base `/etc/plasmalogin.conf` wins over conf.d, and CachyOS ships it
|
||||
with `Session=plasma`.**
|
||||
Every tool that changes sessions (`steamos-session-select`, Steam's power
|
||||
menu, and `cachyos-gamescope-autologin.service`, which resets you to
|
||||
gamescope after a desktop session) only writes
|
||||
`/etc/plasmalogin.conf.d/zz-steamos-autologin.conf`. The base file takes
|
||||
priority, so none of those switches stick.
|
||||
|
||||
The script sets `Session=`, `User=` and `Relogin=true` in the base config,
|
||||
and installs a small systemd path watcher that copies whatever CachyOS's
|
||||
tools write to the conf.d fragment into the base config.
|
||||
|
||||
### What each component changes
|
||||
|
||||
**SteamOS conversion** (`lib/login-manager.sh`, `lib/steam-desktop.sh`,
|
||||
`lib/desktop-shortcut.sh`):
|
||||
|
||||
- installs missing packages: `gamescope-session-cachyos`, `steam`,
|
||||
`mangohud`, `xterm`, `ttf-liberation`, `wqy-zenhei`, `plasma-keyboard`;
|
||||
- *SDDM (single user mode):* installs/enables SDDM and writes its autologin
|
||||
config;
|
||||
- *plasma-login-manager:* creates `/etc/plasmalogin.conf.d`, backs up and
|
||||
rewrites the `[Autologin]` section of `/etc/plasmalogin.conf`, removes
|
||||
stray `zzz-steamos-autologin*.conf` files from manual troubleshooting
|
||||
(never `zz-steamos-autologin.conf`, which CachyOS's tools own), and
|
||||
installs `/usr/local/bin/sync-steamos-session.sh` plus
|
||||
`sync-steamos-session.path`/`.service`;
|
||||
- `KWIN_IM_SHOW_ALWAYS=1` for the virtual keyboard and a
|
||||
`steam-desktop-autostart` systemd user service that starts Steam silently
|
||||
in Plasma only;
|
||||
- the **Return to Gaming Mode** shortcut; on plasma-login-manager with a
|
||||
narrow sudoers rule (`/etc/sudoers.d/gamescope-session-switch`) so it can
|
||||
restart the login manager without a password prompt.
|
||||
|
||||
Turning it off restores `/etc/plasmalogin.conf` from its backup, removes the
|
||||
SDDM autologin, sync bridge, shortcut, sudoers rule and Steam autostart, and
|
||||
switches back to plasma-login-manager.
|
||||
|
||||
**Steam Deck/Machine icons:** `STEAM_GAMEPADUI_ARGS="-gamepadui -steamos3"`
|
||||
in `~/.config/environment.d/` (and gamescope-session's own environment
|
||||
file), which makes Steam show Steam Deck button glyphs in gaming mode.
|
||||
|
||||
### SteamOS desktop look
|
||||
|
||||
Installs CachyOS's `cachyos-vapor` package (the SteamOS Vapor theme the
|
||||
CachyOS handheld edition uses) from the CachyOS repository and switches your
|
||||
desktop to its Vapor global theme, including its desktop and window layout
|
||||
(like ticking "Desktop and window layout" in System Settings): Vapor colors
|
||||
and Plasma style, the SteamOS panel and launcher icon, and the Steam Deck
|
||||
wallpaper. Run it from the Plasma desktop: applying the layout needs a
|
||||
running Plasma session.
|
||||
|
||||
It also adds the SteamOS desktop extras that `cachyos-vapor` doesn't ship,
|
||||
taken from the newest `steamdeck-kde-presets` on Valve's SteamOS mirror
|
||||
(checksum verified) and installed to `/usr/local`:
|
||||
|
||||
- **Add to Steam** in the right-click menu of apps, AppImages and `.exe`
|
||||
files, and in the launcher: adds them to Steam as non-Steam games;
|
||||
- **Nested Desktop**: add it to Steam from the launcher, then start it in
|
||||
gaming mode for a Plasma desktop inside gaming mode;
|
||||
- the SteamOS **Return to Gaming Mode** icon (Steam logo with a return arrow);
|
||||
- a window rule that keeps the **Steam keyboard** above other windows;
|
||||
- an empty, password-less **KWallet**, only if you don't have a wallet yet,
|
||||
so nothing asks for a wallet password after autologin.
|
||||
|
||||
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).
|
||||
|
||||
### Front LED bar (Steam Machine)
|
||||
|
||||
The **Steam Machine support** component, only shown on Fremont hardware
|
||||
(DMI `Valve`/`Fremont`, or `OEM`/`F7F` on early units):
|
||||
|
||||
- installs an AUR helper (`yay`) if neither `yay` nor `paru` is present;
|
||||
- installs the kernel headers for every installed kernel;
|
||||
- installs `leds-valve-dkms-git` from the AUR - Valve's own driver from the
|
||||
SteamOS kernel - builds it for every installed kernel (so the LTS kernel
|
||||
works too), and loads it at every boot (`/etc/modules-load.d/leds-valve.conf`);
|
||||
- adds a udev rule (`/etc/udev/rules.d/70-valve-leds-user.rules`) that gives
|
||||
your user the LED files, so Steam, which runs as you, can drive the bar;
|
||||
- console-like power button: it puts the machine to sleep, and it never
|
||||
suspends by itself on mains power (the launcher's Shut Down still shuts down);
|
||||
- installs and enables `steamos-manager`, the service Steam in gaming mode
|
||||
uses for hardware settings (fan, HDMI-CEC, performance); it recognises the
|
||||
Steam Machine from its DMI data.
|
||||
|
||||
Turning it off removes all of that again (the AUR helper is kept).
|
||||
|
||||
The LEDs appear as `/sys/class/leds/valve-leds*`. The driver's Makefile
|
||||
builds against the running kernel (`uname -r`) instead of the kernel DKMS
|
||||
builds for, so the wizard adds a DKMS override
|
||||
(`/etc/dkms/leds-valve-dkms.conf`) that passes DKMS's target kernel in. With
|
||||
it, DKMS rebuilds the driver whenever a kernel or its headers are installed
|
||||
or upgraded. A newly added kernel doesn't come with its headers, so
|
||||
`ensure-kernel-headers.service` checks at every boot and installs any
|
||||
missing `-headers` package, which makes DKMS build the driver for it.
|
||||
|
||||
Troubleshooting:
|
||||
|
||||
```bash
|
||||
dkms status
|
||||
ls -l /sys/class/leds/valve-leds*/
|
||||
sudo dmesg | grep -i valve
|
||||
cat /var/lib/dkms/leds-valve-dkms/0.1/build/make.log
|
||||
journalctl --user -b | grep -i led
|
||||
```
|
||||
|
||||
**BIOS updates** are not part of the wizard. Valve ships the Steam Machine
|
||||
BIOS as `F7F0108.cab` in its `fremont-hw-support` package for fwupd. To
|
||||
install it by hand (keep the machine on mains power and don't interrupt it):
|
||||
|
||||
```bash
|
||||
sudo dmidecode -s bios-version # current version
|
||||
sudo pacman -S fwupd
|
||||
curl -LO https://steamdeck-packages.steamos.cloud/archlinux-mirror/holo-3.9/os/x86_64/fremont-hw-support-20260807.1-1-any.pkg.tar.zst
|
||||
mkdir fhw && tar -I zstd -xf fremont-hw-support-*.pkg.tar.zst -C fhw
|
||||
sudo fwupdmgr install fhw/usr/share/fwupd/remotes.d/fremont/firmware/F7F0108.cab
|
||||
```
|
||||
|
||||
### Manual session control
|
||||
|
||||
```bash
|
||||
steamos-session-select gamescope # switch to gamescope right now
|
||||
steamos-session-select plasma # switch to desktop right now
|
||||
steamos-session-select persistent # remember the last-used session across reboots
|
||||
steamos-session-select oneshot # always start in gamescope (default, like a Deck)
|
||||
```
|
||||
|
||||
With `Relogin=true`, a gamescope session that fails to start is restarted
|
||||
immediately, which can turn into a loop - hence the Ctrl+Alt+F3 escape above.
|
||||
|
||||
### Removing everything
|
||||
|
||||
Run the wizard and untick everything. Afterwards the PC boots to the normal
|
||||
CachyOS login screen again; packages installed for the conversion (Steam,
|
||||
gamescope-session, ...) stay installed.
|
||||
|
||||
### Project layout
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `setup-gamescope-boot.sh` | Entry point: checks, menu, apply, summary, restart |
|
||||
| `lib/menu.sh` | The menu: detect, toggle, plan and apply changes |
|
||||
| `lib/state.sh` | Undo journal for KDE settings (`kset`/`krevert`) |
|
||||
| `lib/common.sh` | Output helpers, prompts, backups, plasmashell handling |
|
||||
| `lib/packages.sh` | Required packages, AUR helper (yay/paru) |
|
||||
| `lib/login-manager.sh` | SteamOS conversion: SDDM or plasmalogin autologin, sync bridge |
|
||||
| `lib/steam-desktop.sh` | Steam in the Plasma session; Steam Deck/Machine icons |
|
||||
| `lib/desktop-shortcut.sh` | Return to Gaming Mode shortcut and its sudoers rule |
|
||||
| `lib/vapor-theme.sh` | SteamOS theme: installs and switches to `cachyos-vapor` |
|
||||
| `lib/steamos-extras.sh` | SteamOS desktop extras from Valve's package (Add to Steam, Nested Desktop, icon, keyboard rule, KWallet) |
|
||||
| `lib/single-user.sh` | Single user mode: no lock screen, user switching or log out |
|
||||
| `lib/steam-machine.sh` | Steam Machine support: LED driver, LED access, steamos-manager |
|
||||
| `.github/tools/bundle.sh` | Builds the single-file version (`dist/setup-gamescope-boot.sh`) |
|
||||
| `.github/workflows/bundle.yml` | Builds and checks it on every push; publishes it on `main` |
|
||||
|
||||
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
|
||||
shellcheck, and publishes it as a release per version (tag `v<VERSION>`,
|
||||
never overwritten; the newest is marked latest, and the `latest` tag follows
|
||||
it). It inlines
|
||||
`lib/*.sh` and wraps the entry point in `main()`, so bash has read the whole
|
||||
file before anything runs; when stdin is a pipe (`curl | bash`) it reattaches
|
||||
the terminal for the menu (set `WIZARD_KEEP_STDIN=1` to keep piped input).
|
||||
|
||||
Notes for contributors and AI coding agents are in [`AGENTS.md`](AGENTS.md).
|
||||
|
||||
### Caveats
|
||||
|
||||
- This works around CachyOS/`plasma-login-manager` behavior as of September
|
||||
2026. If CachyOS fixes `steam-set-session` upstream, parts of this script
|
||||
become unnecessary - please open an issue or PR if you notice that.
|
||||
- Tested on a Valve Steam Machine running CachyOS Desktop edition. It should
|
||||
work on any CachyOS desktop install using `plasma-login-manager`, but
|
||||
hasn't been tested on Steam Deck/Legion Go hardware.
|
||||
|
||||
### Related upstream reports
|
||||
|
||||
- [CachyOS/gamescope-session#9](https://github.com/CachyOS/gamescope-session/issues/9) - "Switch to Desktop" hang due to missing `/etc/plasmalogin.conf.d`
|
||||
More in [TROUBLESHOOTING.md](TROUBLESHOOTING.md). How it all works:
|
||||
[TECHNICAL.md](TECHNICAL.md). Changes per version: [CHANGELOG.md](CHANGELOG.md).
|
||||
Contributing (or an AI agent)? Read [AGENTS.md](AGENTS.md).
|
||||
|
||||
## License
|
||||
|
||||
MIT - do whatever you want with it.
|
||||
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.
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
# Technical details
|
||||
|
||||
How Steamify CachyOS works under the hood. For using it, see
|
||||
[README.md](README.md); for problems, [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
|
||||
|
||||
## How turning things on and off works
|
||||
|
||||
At startup every component checks whether it is on (`*_status` in `lib/`).
|
||||
You choose; the wizard then turns off what you unticked (in reverse order)
|
||||
and turns on what you ticked. To be able to undo:
|
||||
|
||||
- **System files** are backed up once, next to the original, with a
|
||||
`.bak-gamescope-wizard` suffix, and restored when you turn the component
|
||||
off.
|
||||
- **KDE settings** in your home directory are recorded with their previous
|
||||
value the first time the wizard changes them, in an undo journal under
|
||||
`~/.local/state/cachyos-gamescope-boot/`. Turning the component off writes
|
||||
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
|
||||
KDE's defaults.
|
||||
- **Packages** installed for the conversion (Steam, gamescope-session, ...)
|
||||
are kept when you turn it off; Steam Machine support removes its own.
|
||||
|
||||
## Single user mode: SDDM, no locking
|
||||
|
||||
Turning it on switches to **SDDM** and turns off everything that asks for a
|
||||
password or another user, per user: `action/lock_screen`, `switch_user` and
|
||||
`start_new_session` restrictions in `kdeglobals`, no automatic locking
|
||||
(`kscreenlockerrc`), Meta+L and Ctrl+Alt+Del unbound, and the launcher shows
|
||||
only Sleep / Restart / Shut Down (kickoff `primaryActions=3`), which hides
|
||||
the Session dropdown with Log Out. (Restricting `action/logout` would also
|
||||
hide Restart and Shut Down.) Turning it off restores all of that and moves
|
||||
the conversion back to plasma-login-manager.
|
||||
|
||||
**KDE wallet.** KDE normally unlocks your wallet with the password you log in
|
||||
with; with autologin nobody types it, so apps that keep passwords in it (Brave,
|
||||
for example) ask for the wallet password. Single user mode does what SteamOS
|
||||
does: it uses Valve's empty wallet without a password (from
|
||||
`steamdeck-kde-presets`, checksum verified). Your own wallet in
|
||||
`~/.local/share/kwalletd/` is moved to `kdewallet.kwl.bak-steamify` (and
|
||||
`.salt`). Turning single user mode off puts it back, unchanged; the single
|
||||
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.
|
||||
Passwords aren't shared between the two wallets.
|
||||
|
||||
**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
|
||||
honours. The script installs and enables `sddm` (active from the next boot),
|
||||
writes `User=`, `Session=` and `Relogin=true` to
|
||||
`/etc/sddm.conf.d/10-gamescope-autologin.conf`, and removes any
|
||||
`[Autologin]` from `/etc/sddm.conf` (read last, so it would override both).
|
||||
No sync bridge or sudoers rule is needed; the shortcut just runs
|
||||
`steamos-session-select gamescope`, which logs out, and `Relogin=true` logs
|
||||
straight back in to gamescope.
|
||||
|
||||
Without single user mode, the conversion uses **plasma-login-manager**
|
||||
(CachyOS's default since March 2026), which needs the workarounds below.
|
||||
|
||||
## Why this is needed (plasma-login-manager)
|
||||
|
||||
CachyOS's `gamescope-session-cachyos` package ships `steamos-session-select`,
|
||||
which is meant to work exactly like it does on real SteamOS. Since the March
|
||||
2026 release, CachyOS uses `plasma-login-manager` instead of SDDM, and three
|
||||
things get in the way:
|
||||
|
||||
1. **No `User=` written to the autologin config.**
|
||||
`steam-set-session` (called by `steamos-session-select`) only writes a
|
||||
`Session=` key. `plasma-login-manager` needs both `Session=` and `User=`
|
||||
to autologin at all; without `User=` it shows the greeter every boot.
|
||||
|
||||
2. **Missing `/etc/plasmalogin.conf.d` directory.**
|
||||
Without it, `steam-set-session` fails, and Steam's "Switch to Desktop"
|
||||
hangs forever. *(Fixed upstream in `gamescope-session-cachyos` 1.1.6; the
|
||||
script still creates the directory for older versions.)*
|
||||
|
||||
3. **The base `/etc/plasmalogin.conf` wins over conf.d, and CachyOS ships it
|
||||
with `Session=plasma`.**
|
||||
Every tool that changes sessions (`steamos-session-select`, Steam's power
|
||||
menu, and `cachyos-gamescope-autologin.service`, which resets you to
|
||||
gamescope after a desktop session) only writes
|
||||
`/etc/plasmalogin.conf.d/zz-steamos-autologin.conf`. The base file takes
|
||||
priority, so none of those switches stick.
|
||||
|
||||
The script sets `Session=`, `User=` and `Relogin=true` in the base config,
|
||||
and installs a small systemd path watcher that copies whatever CachyOS's
|
||||
tools write to the conf.d fragment into the base config.
|
||||
|
||||
## What each component changes
|
||||
|
||||
**SteamOS conversion** (`lib/login-manager.sh`, `lib/steam-desktop.sh`,
|
||||
`lib/desktop-shortcut.sh`):
|
||||
|
||||
- installs missing packages: `gamescope-session-cachyos`, `steam`,
|
||||
`mangohud`, `xterm`, `ttf-liberation`, `wqy-zenhei`, `plasma-keyboard`;
|
||||
- *SDDM (single user mode):* installs/enables SDDM and writes its autologin
|
||||
config;
|
||||
- *plasma-login-manager:* creates `/etc/plasmalogin.conf.d`, backs up and
|
||||
rewrites the `[Autologin]` section of `/etc/plasmalogin.conf`, removes
|
||||
stray `zzz-steamos-autologin*.conf` files from manual troubleshooting
|
||||
(never `zz-steamos-autologin.conf`, which CachyOS's tools own), and
|
||||
installs `/usr/local/bin/sync-steamos-session.sh` plus
|
||||
`sync-steamos-session.path`/`.service`;
|
||||
- `KWIN_IM_SHOW_ALWAYS=1` for the virtual keyboard and a
|
||||
`steam-desktop-autostart` systemd user service that starts Steam silently
|
||||
in Plasma only;
|
||||
- the **Return to Gaming Mode** shortcut; on plasma-login-manager with a
|
||||
narrow sudoers rule (`/etc/sudoers.d/gamescope-session-switch`) so it can
|
||||
restart the login manager without a password prompt.
|
||||
|
||||
Turning it off restores `/etc/plasmalogin.conf` from its backup, removes the
|
||||
SDDM autologin, sync bridge, shortcut, sudoers rule and Steam autostart, and
|
||||
switches back to plasma-login-manager.
|
||||
|
||||
**Steam Deck/Machine icons:** `STEAM_GAMEPADUI_ARGS="-gamepadui -steamos3"`
|
||||
in `~/.config/environment.d/` (and gamescope-session's own environment
|
||||
file), which makes Steam show Steam Deck button glyphs in gaming mode.
|
||||
|
||||
## SteamOS desktop look
|
||||
|
||||
Installs CachyOS's `cachyos-vapor` package (the SteamOS Vapor theme the
|
||||
CachyOS handheld edition uses) from the CachyOS repository and switches your
|
||||
desktop to its Vapor global theme, including its desktop and window layout
|
||||
(like ticking "Desktop and window layout" in System Settings): Vapor colors
|
||||
and Plasma style, the SteamOS panel and launcher icon, and the Steam Deck
|
||||
wallpaper. Run it from the Plasma desktop: applying the layout needs a
|
||||
running Plasma session.
|
||||
|
||||
It also adds the SteamOS desktop extras that `cachyos-vapor` doesn't ship,
|
||||
taken from the newest `steamdeck-kde-presets` on Valve's SteamOS mirror
|
||||
(checksum verified) and installed to `/usr/local`:
|
||||
|
||||
- **Add to Steam** in the right-click menu of apps, AppImages and `.exe`
|
||||
files, and in the launcher: adds them to Steam as non-Steam games;
|
||||
- **Nested Desktop**: add it to Steam from the launcher, then start it in
|
||||
gaming mode for a Plasma desktop inside gaming mode;
|
||||
- the SteamOS **Return to Gaming Mode** icon (Steam logo with a return arrow);
|
||||
- a window rule that keeps the **Steam keyboard** above other windows.
|
||||
|
||||
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).
|
||||
|
||||
## Front LED bar (Steam Machine)
|
||||
|
||||
The **Steam Machine support** component, only shown on Fremont hardware
|
||||
(DMI `Valve`/`Fremont`, or `OEM`/`F7F` on early units):
|
||||
|
||||
- installs an AUR helper (`yay`) if neither `yay` nor `paru` is present;
|
||||
- installs the kernel headers for every installed kernel;
|
||||
- installs `leds-valve-dkms-git` from the AUR - Valve's own driver from the
|
||||
SteamOS kernel - builds it for every installed kernel (so the LTS kernel
|
||||
works too), and loads it at every boot (`/etc/modules-load.d/leds-valve.conf`);
|
||||
- adds a udev rule (`/etc/udev/rules.d/70-valve-leds-user.rules`) that gives
|
||||
your user the LED files, so Steam, which runs as you, can drive the bar;
|
||||
- console-like power button: it puts the machine to sleep, and it never
|
||||
suspends by itself on mains power (the launcher's Shut Down still shuts down);
|
||||
- installs and enables `steamos-manager`, the service Steam in gaming mode
|
||||
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
|
||||
DMI data.
|
||||
|
||||
Turning it off removes all of that again (the AUR helper is kept).
|
||||
|
||||
The LEDs appear as `/sys/class/leds/valve-leds*`. The driver's Makefile
|
||||
builds against the running kernel (`uname -r`) instead of the kernel DKMS
|
||||
builds for, so the wizard adds a DKMS override
|
||||
(`/etc/dkms/leds-valve-dkms.conf`) that passes DKMS's target kernel in. With
|
||||
it, DKMS rebuilds the driver whenever a kernel or its headers are installed
|
||||
or upgraded. A newly added kernel doesn't come with its headers, so
|
||||
`ensure-kernel-headers.service` checks at every boot and installs any
|
||||
missing `-headers` package, which makes DKMS build the driver for it.
|
||||
|
||||
## HDMI-CEC
|
||||
|
||||
The **HDMI-CEC** item (on every PC; ticked by default only on a Steam
|
||||
Machine) installs
|
||||
Valve's CEC stack from its SteamOS `holo` repository, the newest `holo-X.Y`
|
||||
on `steamdeck-packages.steamos.cloud`, each package checked against the
|
||||
SHA-256 in Valve's package index:
|
||||
|
||||
- `cecd`, Valve's CEC daemon (a user service started with the desktop and
|
||||
gaming mode): TV remote keys become normal key presses (arrows, Enter,
|
||||
Back), and it turns the TV on and off with the PC;
|
||||
- `cec-audio-control`: volume of the TV or receiver;
|
||||
- `inputattach-cec-units` (with `linuxconsole` from CachyOS for
|
||||
`inputattach`): attaches USB CEC adapters (Pulse-Eight, RainShadow).
|
||||
|
||||
It works with any `/dev/cec*`: a GPU that has CEC on its HDMI port (the
|
||||
Steam Machine; the CachyOS kernel has DisplayPort CEC built in) or a USB
|
||||
adapter ([CEC.md](CEC.md) lists which PCs have one). The menu lists the CEC
|
||||
devices found. Also turn on CEC on the TV
|
||||
(Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink). On a Steam Machine
|
||||
`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
|
||||
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
|
||||
after it goes to sleep (Samsung TVs,
|
||||
[SteamOS#2626](https://github.com/ValveSoftware/SteamOS/issues/2626)) and
|
||||
upset CEC for the TV's other devices
|
||||
([SteamOS#2817](https://github.com/ValveSoftware/SteamOS/issues/2817)).
|
||||
Turning it off removes the packages again (`linuxconsole` only if the
|
||||
wizard installed it).
|
||||
|
||||
## Power-off fix (Steam Machine)
|
||||
|
||||
With recent kernels a Steam Machine starts again right after powering off:
|
||||
the newer 6.x, 7.0 and 7.1 updates and 7.2 (and probably every kernel after
|
||||
it). Linux stopped clearing the S4/S5 wake bits at probe
|
||||
(`pinctrl-amd: Don't clear S4 wake bits at probe`, in 7.2 and backported to
|
||||
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 **Power-off fix** sub-option of Steam Machine support (ticked along with
|
||||
it, can be unticked) builds a small module with DKMS for every installed
|
||||
kernel, `steamify-fremont-poweroff` (source in
|
||||
[`patches/steamify-fremont-poweroff.c`](patches/steamify-fremont-poweroff.c)).
|
||||
It only loads on Fremont (DMI board name) and touches one register: right
|
||||
before power-off (a `SYS_OFF_MODE_POWER_OFF_PREPARE` handler, after the
|
||||
drivers have shut down) it clears that pin's S4/S5 wake bit. On a kernel
|
||||
that already clears it, it does nothing. It logs the pin's state at load:
|
||||
`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.
|
||||
|
||||
Where Steam Machine support was set up before 2.2.0, the menu ticks the
|
||||
power-off fix as a new default sub-option (see feature versions below), so a
|
||||
normal run adds it.
|
||||
|
||||
## 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,
|
||||
NVIDIA's driver doesn't: with an NVIDIA card the option is shown greyed
|
||||
out, with why (`WIZARD_VRAM_FAKE_NVIDIA=1` fakes that for tests). Otherwise
|
||||
the **VRAM booster** option is only shown when
|
||||
`/sys/fs/cgroup/dmem.capacity` lists a VRAM region 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/cachyos-gamescope-boot/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)
|
||||
|
||||
The **Update BIOS** item is only shown on a Steam Machine and is never ticked
|
||||
by default. It shows the BIOS version you have now and the newest one Valve
|
||||
ships (the `.cab` file in its `fremont-hw-support` package, looked up on
|
||||
Valve's SteamOS mirror):
|
||||
|
||||
```
|
||||
[ ] Update BIOS: now F7F0107, newest F7F0108 (own risk) (opt-in, runs once)
|
||||
```
|
||||
|
||||
It can only be ticked when Valve has a newer BIOS than yours; when you're up
|
||||
to date, when the newest version can't be looked up (offline), or when an
|
||||
update is already waiting for a restart, it's greyed out.
|
||||
|
||||
When you run it, the wizard:
|
||||
|
||||
1. downloads Valve's package and checks its **SHA-256** against Valve's
|
||||
repository, so it's exactly Valve's file;
|
||||
2. asks **fwupd** whether the firmware is for this very machine (fwupd compares
|
||||
the firmware's hardware IDs with the device) and stops if it isn't;
|
||||
3. shows a large red **warning** with the current and the new version, and asks
|
||||
whether you understand the risks (default: no);
|
||||
4. shows the warning **again** and only continues when you type `UPDATE`;
|
||||
5. hands the firmware to fwupd, which writes it during the **next restart**:
|
||||
choose "restart now" or restart later yourself.
|
||||
|
||||
**At your own risk:** a failed or interrupted BIOS update can leave the machine
|
||||
unable to start. Keep it on mains power, and never turn off the power, unplug
|
||||
it or press the power button while it updates, including during the restart;
|
||||
the screen can stay black for several minutes.
|
||||
|
||||
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
|
||||
warnings, skips fwupd's device check, and only prints the install command.
|
||||
|
||||
## Manual session control
|
||||
|
||||
```bash
|
||||
steamos-session-select gamescope # switch to gamescope right now
|
||||
steamos-session-select plasma # switch to desktop right now
|
||||
steamos-session-select persistent # remember the last-used session across reboots
|
||||
steamos-session-select oneshot # always start in gamescope (default, like a Deck)
|
||||
```
|
||||
|
||||
With `Relogin=true`, a gamescope session that fails to start is restarted
|
||||
immediately, which can turn into a loop - see
|
||||
[TROUBLESHOOTING.md](TROUBLESHOOTING.md) for the way out.
|
||||
|
||||
## Project layout
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `steamify.sh` | Entry point: checks, menu, apply, summary, restart |
|
||||
| `lib/menu.sh` | The menu: detect, toggle, plan and apply changes |
|
||||
| `lib/state.sh` | Undo journal for KDE settings (`kset`/`krevert`) |
|
||||
| `lib/common.sh` | Output helpers, prompts, backups, plasmashell handling |
|
||||
| `lib/packages.sh` | Required packages, AUR helper (yay/paru) |
|
||||
| `lib/boot-session.sh` | Boot into gamescope or the desktop (`steamify-boot-desktop.service`) |
|
||||
| `lib/login-manager.sh` | SteamOS conversion: SDDM or plasmalogin autologin, sync bridge |
|
||||
| `lib/steam-desktop.sh` | Steam in the Plasma session; Steam Deck/Machine icons |
|
||||
| `lib/desktop-shortcut.sh` | Return to Gaming Mode shortcut and its sudoers rule |
|
||||
| `lib/vapor-theme.sh` | SteamOS theme: installs and switches to `cachyos-vapor` |
|
||||
| `lib/steamos-extras.sh` | SteamOS desktop extras from Valve's package (Add to Steam, Nested Desktop, icon, keyboard rule, KWallet) |
|
||||
| `lib/single-user.sh` | Single user mode: no lock screen, user switching or log out |
|
||||
| `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/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 |
|
||||
| `lib/fremont-poweroff.sh` | Steam Machine support: the power-off fix (DKMS module from `patches/`) |
|
||||
| `lib/vram-booster.sh` | VRAM booster (`dmemcg-booster`, `plasma-foreground-booster`) |
|
||||
| `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/` embedded |
|
||||
| `.github/workflows/bundle.yml` | Builds and checks it on every push; publishes it on `main` |
|
||||
|
||||
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
|
||||
shellcheck, and publishes it as a release per version (tag `v<VERSION>`,
|
||||
never overwritten; the newest is marked latest, and the `latest` tag follows
|
||||
it). It inlines
|
||||
`lib/*.sh` and wraps the entry point in `main()`, so bash has read the whole
|
||||
file before anything runs; when stdin is a pipe (`curl | bash`) it reattaches
|
||||
the terminal for the menu (set `WIZARD_KEEP_STDIN=1` to keep piped input).
|
||||
|
||||
Notes for contributors and AI coding agents are in [`AGENTS.md`](AGENTS.md).
|
||||
|
||||
## Caveats
|
||||
|
||||
- This works around CachyOS/`plasma-login-manager` behavior as of September
|
||||
2026. If CachyOS fixes `steam-set-session` upstream, parts of this script
|
||||
become unnecessary - please open an issue or PR if you notice that.
|
||||
- Tested on a Valve Steam Machine running CachyOS Desktop edition. It should
|
||||
work on any CachyOS desktop install using `plasma-login-manager`, but
|
||||
hasn't been tested on Steam Deck/Legion Go hardware.
|
||||
|
||||
## Related upstream reports
|
||||
|
||||
- [CachyOS/gamescope-session#9](https://github.com/CachyOS/gamescope-session/issues/9) - "Switch to Desktop" hang due to missing `/etc/plasmalogin.conf.d`
|
||||
@@ -0,0 +1,75 @@
|
||||
# Troubleshooting
|
||||
|
||||
See [TECHNICAL.md](TECHNICAL.md) for how each part works.
|
||||
|
||||
## Gaming mode shows a black screen or keeps restarting
|
||||
|
||||
Press **Ctrl+Alt+F3**, log in with your username and password, and run:
|
||||
|
||||
```bash
|
||||
sudo /usr/lib/steamos/steam-set-session plasma.desktop && sudo systemctl restart display-manager
|
||||
```
|
||||
|
||||
That takes you back to the desktop. Gamescope sessions that fail to start are
|
||||
restarted right away (`Relogin=true`), which is why it can loop.
|
||||
|
||||
## Something is set up wrong
|
||||
|
||||
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.
|
||||
|
||||
## The LED bar on the Steam Machine stays dark
|
||||
|
||||
Restart once more; a freshly installed driver often needs a reboot. The menu
|
||||
shows per kernel whether the LED driver is built, and whether it's loaded.
|
||||
Still dark:
|
||||
|
||||
```bash
|
||||
dkms status
|
||||
ls -l /sys/class/leds/valve-leds*/
|
||||
sudo dmesg | grep -i valve
|
||||
cat /var/lib/dkms/leds-valve-dkms/0.1/build/make.log
|
||||
journalctl --user -b | grep -i led
|
||||
```
|
||||
|
||||
## The TV remote does nothing (HDMI-CEC)
|
||||
|
||||
- Turn on CEC in the TV's settings (Sony: BRAVIA Sync, Samsung: Anynet+, LG:
|
||||
SimpLink) and, if it has one, allow the PC to control it.
|
||||
- Check that the PC has a CEC device, and that `cecd` runs:
|
||||
|
||||
```bash
|
||||
ls /dev/cec*
|
||||
systemctl --user status cecd
|
||||
journalctl --user -u cecd -b
|
||||
cec-ctl -d /dev/cec0 -S # the devices on the TV's CEC bus (v4l-utils)
|
||||
```
|
||||
|
||||
No `/dev/cec*`: your PC has no CEC; a USB CEC adapter (e.g. Pulse-Eight)
|
||||
works. See [CEC.md](CEC.md). The Steam Machine wakes up again right after going
|
||||
to sleep, or other devices lose CEC? That happens on SteamOS too; turn
|
||||
HDMI-CEC off in the wizard.
|
||||
|
||||
## The Steam Machine still reboots when I shut it down
|
||||
|
||||
Check that the pinned kernel is installed and running:
|
||||
|
||||
```bash
|
||||
uname -r # should be 7.1.6-1-cachyos
|
||||
pacman -Q linux-cachyos linux-cachyos-headers
|
||||
grep '^IgnorePkg' /etc/pacman.conf # should list both packages
|
||||
```
|
||||
|
||||
Not running it yet? Restart. Not installed? Run the wizard and make sure
|
||||
**Pin the kernel** under Steam Machine support is ticked. If you have more
|
||||
kernels installed (e.g. the LTS kernel), pick `linux-cachyos` in the boot menu.
|
||||
|
||||
## The kernel pin can't download or verify the kernel
|
||||
|
||||
The wizard tries this repo's `kernel-7.1.6-1` release, then the CachyOS
|
||||
archive and mirror. A file that fails its checksum or CachyOS signature is
|
||||
deleted, so running the wizard again downloads it once more. Offline? Put
|
||||
the four files from the
|
||||
[kernel-7.1.6-1 release](https://github.com/theupriser/steamify-cachyos/releases/tag/kernel-7.1.6-1)
|
||||
in `/var/cache/steamify/kernel` and run it again.
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Icon for the "Steamify CachyOS" shortcut (cachyos-gamescope-boot).
|
||||
Derived from Valve's steam-gaming-return.svg (steamdeck-kde-presets,
|
||||
GPL-2.0): the Return to Gaming Mode arrow replaced by a gear. -->
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
version="1.1"
|
||||
id="svg11"
|
||||
sodipodi:docname="steam-gaming-settings.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview11"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="true"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="16.6875"
|
||||
inkscape:cy="16.4375"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1014"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11">
|
||||
<inkscape:grid
|
||||
id="grid11"
|
||||
units="px"
|
||||
originx="0"
|
||||
originy="0"
|
||||
spacingx="1"
|
||||
spacingy="1"
|
||||
empcolor="#3f3fff"
|
||||
empopacity="0.25098039"
|
||||
color="#3f3fff"
|
||||
opacity="0.1254902"
|
||||
empspacing="5"
|
||||
enabled="true"
|
||||
visible="true" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs4">
|
||||
<linearGradient
|
||||
id="blue-gradient-5">
|
||||
<stop
|
||||
style="stop-color:#054e87;stop-opacity:1"
|
||||
offset="0"
|
||||
id="stop1" />
|
||||
<stop
|
||||
style="stop-color:#1d99f3;stop-opacity:1"
|
||||
offset="1"
|
||||
id="stop2" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="shadow-3">
|
||||
<stop
|
||||
style="stop-color:#31363b;stop-opacity:0.208"
|
||||
offset="0"
|
||||
id="stop3" />
|
||||
<stop
|
||||
style="stop-color:#31363b;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
xlink:href="#shadow-3"
|
||||
id="linearGradient4199"
|
||||
x1="21.969"
|
||||
y1="1035.362"
|
||||
x2="35.969"
|
||||
y2="1049.362"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(10,-1018.3622)" />
|
||||
<linearGradient
|
||||
xlink:href="#shadow-3"
|
||||
id="linearGradient4203"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="23"
|
||||
y1="1035.362"
|
||||
x2="36"
|
||||
y2="1048.362"
|
||||
gradientTransform="translate(13,-1021.3622)" />
|
||||
<linearGradient
|
||||
xlink:href="#shadow-3"
|
||||
id="linearGradient4227"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-15.75,-1005.8622)"
|
||||
x1="21.475"
|
||||
y1="1035.158"
|
||||
x2="34.475"
|
||||
y2="1048.158" />
|
||||
<linearGradient
|
||||
xlink:href="#blue-gradient-5"
|
||||
id="linearGradient4351"
|
||||
x1="24"
|
||||
y1="1008.362"
|
||||
x2="24"
|
||||
y2="1048.362"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(0,-1004.3622)" />
|
||||
<linearGradient
|
||||
xlink:href="#shadow-3"
|
||||
id="linearGradient4373"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(5.9783302,-1021.8757)"
|
||||
x1="21.085"
|
||||
y1="1036.975"
|
||||
x2="35.969"
|
||||
y2="1049.362" />
|
||||
<linearGradient
|
||||
xlink:href="#shadow-3"
|
||||
id="linearGradient4377"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-6.3297472,-1009.2141)"
|
||||
x1="21.085"
|
||||
y1="1036.975"
|
||||
x2="35.969"
|
||||
y2="1049.362" />
|
||||
<linearGradient
|
||||
xlink:href="#shadow-3"
|
||||
id="linearGradient4381"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(-2.8703079,-1005.5321)"
|
||||
x1="21.969"
|
||||
y1="1035.362"
|
||||
x2="35.969"
|
||||
y2="1049.362" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#blue-gradient-5"
|
||||
id="linearGradient12"
|
||||
x1="34.848072"
|
||||
y1="2.7402468"
|
||||
x2="35.248451"
|
||||
y2="46.980125"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#shadow-3"
|
||||
id="linearGradient14"
|
||||
x1="34.345119"
|
||||
y1="37.896885"
|
||||
x2="42.498322"
|
||||
y2="45.409893"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#shadow-3"
|
||||
id="linearGradient16"
|
||||
x1="3.5050631"
|
||||
y1="46.96999"
|
||||
x2="3.6629491"
|
||||
y2="50.93874"
|
||||
gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<circle
|
||||
style="fill:url(#linearGradient4351);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
cx="24"
|
||||
cy="23.999861"
|
||||
r="20"
|
||||
id="circle4" />
|
||||
<circle
|
||||
id="path43518"
|
||||
cx="34.995033"
|
||||
cy="34.995033"
|
||||
style="fill:url(#linearGradient12);fill-rule:evenodd;stroke-width:0.999996;fill-opacity:1"
|
||||
r="12" />
|
||||
<path
|
||||
style="fill:url(#linearGradient4203);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 35.923828,13.330078 C 37.206994,14.675942 38,16.493653 38,18.5 c -10e-7,4.142136 -3.357865,7.5 -7.5,7.5 -0.203438,-0.0015 -0.406734,-0.01127 -0.609375,-0.0293 l -5.896484,5.111328 c -0.0083,0.739616 -0.153346,1.471317 -0.427735,2.158203 -1.23712,3.074359 -4.732395,4.563589 -7.80664,3.326172 -0.24293,-0.09775 -0.470788,-0.215212 -0.69336,-0.339844 -0.04331,-0.0244 -0.08621,-0.04875 -0.128906,-0.07422 -0.211403,-0.125374 -0.414371,-0.260582 -0.605469,-0.408203 -0.0063,-0.0049 -0.01326,-0.0088 -0.01953,-0.01367 l -0.01172,0.002 8.19336,8.193359 C 22.994984,43.969391 23.497293,43.994196 24,44 a 20,20 0 0 0 1.310547,-0.04687 c 0.06663,-0.0043 0.132772,-0.01066 0.199219,-0.01563 a 20,20 0 0 0 1.845703,-0.222656 c 0.03199,-0.0054 0.06377,-0.01202 0.0957,-0.01758 a 20,20 0 0 0 1.396484,-0.3125 c 0.240477,-0.05998 0.481808,-0.116947 0.71875,-0.185547 A 20,20 0 0 0 30.7832,42.79492 c 0.221103,-0.07984 0.444706,-0.154851 0.662109,-0.242188 a 20,20 0 0 0 1.148441,-0.51367 C 32.813074,41.93437 33.035177,41.835015 33.25,41.722656 a 20,20 0 0 0 1.03125,-0.591797 c 0.231705,-0.139379 0.46789,-0.271541 0.693359,-0.419921 a 20,20 0 0 0 0.953125,-0.685547 c 0.206289,-0.153802 0.419094,-0.299475 0.619141,-0.460938 a 20,20 0 0 0 0.908203,-0.796875 c 0.190109,-0.173313 0.387023,-0.339092 0.570313,-0.519531 a 20,20 0 0 0 0.833984,-0.892578 c 0.163426,-0.181706 0.335418,-0.355335 0.492187,-0.542969 a 20,20 0 0 0 0.863282,-1.136719 c 0.102363,-0.141895 0.215754,-0.275269 0.314453,-0.419922 a 20,20 0 0 0 3.371094,-9.257812 c 6.3e-5,-6.43e-4 -6.4e-5,-0.0013 0,-0.002 a 20,20 0 0 0 0,-0.0059 A 20,20 0 0 0 44,24 20,20 0 0 0 43.779297,21.185547 Z"
|
||||
id="path6" />
|
||||
<path
|
||||
style="fill:url(#linearGradient4227);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 5.7695312,29.3125 19.363281,43.431641 a 20,20 0 0 0 3.13086,0.49414 l -8.19336,-8.193359 -2.001953,-4.97461 z"
|
||||
id="path9" />
|
||||
<g
|
||||
id="g11">
|
||||
<path
|
||||
d="m 26.586,23.6632 8.104641,-0.4201 1.91027,-6.7552 -5.59099,-4.4419 -6.871493,5.8329 z"
|
||||
style="fill:url(#linearGradient4373);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path4" />
|
||||
<path
|
||||
style="fill:url(#linearGradient4199);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 31.027515,23.9946 35,23 l 1.976562,-2.9766 -4,-4 -4.949047,4.9712 z"
|
||||
id="path5" />
|
||||
<path
|
||||
style="fill:url(#linearGradient4377);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M 15.316486,35.7061 21.675457,35.065 23.0333,27.5143 16.403747,25.7462 12.603749,30.3195 Z"
|
||||
id="path7" />
|
||||
<path
|
||||
d="m 16.985332,35.8717 3.972485,-0.4165 2.210937,-3.4453 -3.0625,-3.1563 -6.933422,3.9868 z"
|
||||
style="fill:url(#linearGradient4381);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="path8" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 30.5,11 c -4.142135,0 -7.499999,3.357864 -7.5,7.5 0.0017,0.201484 0.01143,0.402819 0.0293,0.603516 -1.289636,1.575722 -4.80544,5.546997 -5.113281,5.902343 -2.416761,0.03284 -4.577852,1.51258 -5.482422,3.753907 -1.237432,3.074241 0.2518,6.569519 3.326172,7.80664 3.074245,1.237417 6.56952,-0.251813 7.80664,-3.326172 0.274389,-0.686886 0.419405,-1.418587 0.427735,-2.158203 l 5.896484,-5.111328 C 30.093269,25.98873 30.296562,25.998503 30.5,26 34.642135,26 37.999999,22.642136 38,18.5 37.999999,14.357864 34.642135,11 30.5,11 Z m 0,3 c 2.48528,0 4.499999,2.014718 4.5,4.5 -10e-7,2.485282 -2.01472,4.5 -4.5,4.5 -2.48528,0 -4.499999,-2.014718 -4.5,-4.5 1e-6,-2.485282 2.01472,-4.5 4.5,-4.5 z M 18.046875,27 c 0.496175,0.0058 0.986949,0.103774 1.447266,0.289062 2.049689,0.825003 3.042263,3.155585 2.216797,5.205079 -0.825003,2.049668 -3.155579,3.04224 -5.205079,2.216797 C 14.45617,33.885935 13.463596,31.555353 14.289062,29.505859 14.905127,27.975627 16.397393,26.980525 18.046875,27 Z"
|
||||
id="path10" />
|
||||
<circle
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
cx="30.5"
|
||||
cy="18.4998"
|
||||
id="ellipse10"
|
||||
r="3.5" />
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 5.0722656,22.564453 A 19,19 0 0 0 5,24 19,19 0 0 0 5.7695312,29.3125 l 11.1113278,4.470703 a 3,3 0 0 0 3.902344,-1.664062 3,3 0 0 0 -1.664062,-3.902344 z"
|
||||
id="path11" />
|
||||
</g>
|
||||
|
||||
|
||||
|
||||
<path id="gear" style="fill:#ffffff;fill-rule:evenodd;stroke:none" d="M 40.440,33.688 L 42.527,33.983 L 42.527,36.007 L 40.440,36.302 L 39.770,37.921 L 41.037,39.606 L 39.606,41.037 L 37.921,39.770 L 36.302,40.440 L 36.007,42.527 L 33.983,42.527 L 33.688,40.440 L 32.069,39.770 L 30.384,41.037 L 28.953,39.606 L 30.220,37.921 L 29.550,36.302 L 27.463,36.007 L 27.463,33.983 L 29.550,33.688 L 30.220,32.069 L 28.953,30.384 L 30.384,28.953 L 32.069,30.220 L 33.688,29.550 L 33.983,27.463 L 36.007,27.463 L 36.302,29.550 L 37.921,30.220 L 39.606,28.953 L 41.037,30.384 L 39.770,32.069 Z M 37.395,34.995 A 2.4,2.4 0 1 0 32.595,34.995 A 2.4,2.4 0 1 0 37.395,34.995 Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 11 KiB |
+186
@@ -0,0 +1,186 @@
|
||||
#!/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+=",\"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" && echo true || echo false)"
|
||||
items+=",\"selectable\":$(component_selectable "$c" && echo true || echo false)}"
|
||||
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.
|
||||
[[ "${WANTED[single]:-0}" == 1 ]] && WANTED[gaming]=1
|
||||
[[ "${WANTED[gaming]:-0}" == 0 ]] && { WANTED[single]=0; WANTED[boot]=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
|
||||
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
|
||||
[[ " ${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
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
#!/bin/bash
|
||||
# "Update BIOS" menu item, only on a Steam Machine and always opt-in: flashes
|
||||
# the newest Steam Machine BIOS from Valve's fremont-hw-support package with
|
||||
# fwupd. It's an action, not an on/off component: never preselected, never
|
||||
# re-applied, and there is nothing to turn off afterwards.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
BIOS_REPO_PREFIX=holo
|
||||
# WIZARD_BIOS_DRY_RUN=1 walks through the whole BIOS update (download,
|
||||
# checksum, both warnings) but never flashes: fwupd's device check is
|
||||
# skipped and the install is only printed. For testing, e.g. in a VM.
|
||||
BIOS_DRY_RUN="${WIZARD_BIOS_DRY_RUN:-}"
|
||||
|
||||
bios_available() { detect_valve_fremont; }
|
||||
|
||||
bios_status() { return 1; }
|
||||
|
||||
bios_disable() { return 0; }
|
||||
|
||||
bios_current() {
|
||||
cat /sys/class/dmi/id/bios_version 2>/dev/null || echo unknown
|
||||
}
|
||||
|
||||
bios_lookup_newest() {
|
||||
# Sets BIOS_REPO, BIOS_PKG, BIOS_SHA256 and BIOS_NEWEST (e.g. F7F0108)
|
||||
# from the newest holo-X.Y repository on Valve's mirror: its .files
|
||||
# database names the firmware file, its .db gives the package checksum.
|
||||
# Only once per run, and with short timeouts so an offline machine
|
||||
# doesn't hold up the menu.
|
||||
[[ -n "${BIOS_LOOKED_UP:-}" ]] && return 0
|
||||
BIOS_LOOKED_UP=1; BIOS_NEWEST=""
|
||||
local tmp desc
|
||||
BIOS_REPO="$(curl -fsL --max-time 10 "$VALVE_MIRROR/" | grep -oE "$BIOS_REPO_PREFIX-[0-9]+\.[0-9]+/" | tr -d / | sort -uV | tail -n 1)"
|
||||
[[ -n "$BIOS_REPO" ]] || return 1
|
||||
tmp="$(mktemp -d)"
|
||||
if curl -fsL --max-time 30 "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_REPO.files" -o "$tmp/files" &&
|
||||
curl -fsL --max-time 30 "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_REPO.db" -o "$tmp/db"; then
|
||||
desc="$(tar -tf "$tmp/files" 2>/dev/null | grep -E '^fremont-hw-support-[0-9][^/]*/files$' | head -n 1)"
|
||||
[[ -n "$desc" ]] && BIOS_CAB="$(tar -xOf "$tmp/files" "$desc" | grep -E '^usr/share/fwupd/.*\.cab$' | head -n 1)"
|
||||
desc="${desc%/files}/desc"
|
||||
BIOS_PKG="$(tar -xOf "$tmp/db" "$desc" 2>/dev/null | awk '/^%FILENAME%$/ { getline; print }')"
|
||||
BIOS_SHA256="$(tar -xOf "$tmp/db" "$desc" 2>/dev/null | awk '/^%SHA256SUM%$/ { getline; print }')"
|
||||
[[ -n "${BIOS_CAB:-}" && -n "$BIOS_PKG" && -n "$BIOS_SHA256" ]] && BIOS_NEWEST="$(basename "$BIOS_CAB" .cab)"
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
[[ -n "$BIOS_NEWEST" ]]
|
||||
}
|
||||
|
||||
bios_selectable() {
|
||||
# Only when Valve has a newer BIOS than the one installed; the menu
|
||||
# greys the item out otherwise (up to date, or newest unknown/offline).
|
||||
[[ -z "${BIOS_NEEDS_RESTART:-}" && -n "${BIOS_NEWEST:-}" && "$BIOS_NEWEST" != "$(bios_current)" ]]
|
||||
}
|
||||
|
||||
bios_label() {
|
||||
# Menu label with the current and the newest version; kept short so the
|
||||
# row (with "(not available)" when greyed out) fits 80 columns.
|
||||
local current
|
||||
current="$(bios_current)"
|
||||
if [[ -n "${BIOS_NEEDS_RESTART:-}" ]]; then
|
||||
echo "Update BIOS: $BIOS_NEWEST waits for a restart"
|
||||
elif [[ -z "${BIOS_NEWEST:-}" ]]; then
|
||||
echo "Update BIOS: now $current, newest unknown (offline?)"
|
||||
elif [[ "$BIOS_NEWEST" == "$current" ]]; then
|
||||
echo "Update BIOS: $current is up to date"
|
||||
else
|
||||
echo "Update BIOS: now $current, newest $BIOS_NEWEST (own risk)"
|
||||
fi
|
||||
}
|
||||
|
||||
BIOS_BOX_WIDTH=68
|
||||
|
||||
bios_box_line() {
|
||||
# bios_box_line [text]: one line of the warning box, padded to the same
|
||||
# visible width (colour codes don't count) so the right edge lines up.
|
||||
local text="${1:-}" plain pad
|
||||
plain="$(printf '%b' "$text" | sed 's/\x1b\[[0-9;]*m//g')"
|
||||
pad=$(( BIOS_BOX_WIDTH - 6 - ${#plain} ))
|
||||
(( pad < 0 )) && pad=0
|
||||
printf ' %b██%b %b%*s%b██%b\n' "$c_red" "$c_reset" "$text" "$pad" "" "$c_red" "$c_reset"
|
||||
}
|
||||
|
||||
bios_box_border() {
|
||||
# Block characters: Konsole draws a long coloured row of # narrower
|
||||
# than the box's other lines, so its right edge wouldn't line up.
|
||||
printf ' %b%s%b\n' "$c_red" "$(printf '█%.0s' $(seq "$BIOS_BOX_WIDTH"))" "$c_reset"
|
||||
}
|
||||
|
||||
bios_disclaimer() {
|
||||
# bios_disclaimer <title> [line...]: the warning box; extra lines are
|
||||
# shown above the fixed risks.
|
||||
local title="$1" line b="$c_bold" n="$c_reset"
|
||||
shift
|
||||
echo
|
||||
bios_box_border
|
||||
bios_box_line
|
||||
bios_box_line "$c_red$b$title$n"
|
||||
bios_box_line
|
||||
bios_box_border
|
||||
for line in "$@"; do bios_box_line "$line"; done
|
||||
bios_box_line
|
||||
bios_box_line "- A failed or interrupted BIOS update can leave the machine"
|
||||
bios_box_line " unable to start (bricked). This wizard, CachyOS and Valve"
|
||||
bios_box_line " take no responsibility for that."
|
||||
bios_box_line "- ${b}NEVER turn off the power, unplug the machine or press${n}"
|
||||
bios_box_line " ${b}the power button while the update runs${n}, including the"
|
||||
bios_box_line " restart(s) afterwards, when the firmware is written."
|
||||
bios_box_line "- The screen can stay black for several minutes. Wait."
|
||||
bios_box_line "- Only on mains power, with all other programs closed."
|
||||
bios_box_line
|
||||
bios_box_border
|
||||
echo
|
||||
}
|
||||
|
||||
bios_fits_device() {
|
||||
# bios_fits_device <cab>: does fwupd match this firmware to a device in
|
||||
# this machine? It compares the file's hardware IDs (GUIDs) with the
|
||||
# hardware; for a file that doesn't fit it reports an UpdateError.
|
||||
local details
|
||||
details="$(sudo fwupdmgr get-details --json "$1" 2>/dev/null)" || return 1
|
||||
grep -q '"Guid"' <<< "$details" && ! grep -q '"UpdateError"' <<< "$details"
|
||||
}
|
||||
|
||||
# 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
|
||||
err "Couldn't find the newest Steam Machine BIOS on Valve's mirror ($VALVE_MIRROR)."
|
||||
return 1
|
||||
fi
|
||||
[[ -n "$BIOS_DRY_RUN" ]] && warn "DRY RUN (WIZARD_BIOS_DRY_RUN): nothing will be flashed."
|
||||
if [[ "$(bios_current)" == "$BIOS_NEWEST" ]]; then
|
||||
ok "The BIOS is already the newest version ($BIOS_NEWEST); nothing to do."
|
||||
return 2
|
||||
fi
|
||||
pacman -Q fwupd >/dev/null 2>&1 || sudo pacman -S --needed --noconfirm fwupd ||
|
||||
{ err "Installing fwupd failed."; return 1; }
|
||||
rm -rf "$BIOS_STAGE_DIR"; mkdir -p "$BIOS_STAGE_DIR"; chmod 700 "$BIOS_STAGE_DIR"
|
||||
info "Downloading $BIOS_PKG ($BIOS_REPO)..."
|
||||
if ! curl -fsSL "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_PKG" -o "$BIOS_STAGE_DIR/pkg.tar.zst" ||
|
||||
! echo "$BIOS_SHA256 $BIOS_STAGE_DIR/pkg.tar.zst" | sha256sum -c --quiet - ||
|
||||
! 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."
|
||||
rm -rf "$BIOS_STAGE_DIR"
|
||||
return 1
|
||||
fi
|
||||
ok "Checksum OK: this is Valve's $BIOS_PKG."
|
||||
if [[ -n "$BIOS_DRY_RUN" ]]; then
|
||||
warn "Dry run: skipping fwupd's check that the firmware fits this machine."
|
||||
BIOS_COMPATIBLE=dry-run
|
||||
elif bios_fits_device "$BIOS_STAGE_DIR/$BIOS_CAB"; then
|
||||
ok "fwupd confirms BIOS $BIOS_NEWEST is firmware for this machine."
|
||||
BIOS_COMPATIBLE=yes
|
||||
else
|
||||
err "fwupd says BIOS $BIOS_NEWEST is not for this machine's hardware; not installing it."
|
||||
rm -rf "$BIOS_STAGE_DIR"
|
||||
return 1
|
||||
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" \
|
||||
"Current BIOS: ${b}$current${n}" \
|
||||
"New BIOS: ${b}$BIOS_NEWEST${n}" \
|
||||
"Checksum: ${yes}OK${n} (Valve's package)" \
|
||||
"Compatible: $compatible"
|
||||
if ! ask_yn "Do you understand the risks and want to continue?" n; then
|
||||
info "BIOS update cancelled; nothing was changed."; rm -rf "$BIOS_STAGE_DIR"; return 0
|
||||
fi
|
||||
bios_disclaimer "LAST CHANCE: THIS FLASHES BIOS $BIOS_NEWEST" \
|
||||
"After this, keep the power on until the machine has fully" \
|
||||
"started again. Don't touch it, even if the screen is black."
|
||||
local 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
|
||||
info "BIOS update cancelled; nothing was changed."; rm -rf "$BIOS_STAGE_DIR"; return 0
|
||||
fi
|
||||
bios_flash
|
||||
}
|
||||
|
||||
# For the app (lib/backend.sh): the flash step alone, after its warnings.
|
||||
bios_flash_only() { bios_flash; }
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# "Boot into: gamescope / desktop" menu item, on top of the SteamOS
|
||||
# conversion. Gamescope is the default (the conversion's autologin, and
|
||||
# CachyOS's cachyos-gamescope-autologin resetting to it after a desktop
|
||||
# session). Desktop: a unit that sets the autologin session to Plasma at
|
||||
# every boot, before the login manager starts. Switching between the two in
|
||||
# a running system (Return to Gaming Mode, Steam's Switch to Desktop) works
|
||||
# the same either way.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
BOOT_UNIT_NAME="steamify-boot-desktop.service"
|
||||
BOOT_UNIT="/etc/systemd/system/$BOOT_UNIT_NAME"
|
||||
SESSION_SYNC="/usr/local/bin/sync-steamos-session.sh"
|
||||
|
||||
boot_status() {
|
||||
# On = boots into the desktop.
|
||||
[[ -f "$BOOT_UNIT" ]] && systemctl is-enabled -q "$BOOT_UNIT_NAME" 2>/dev/null
|
||||
}
|
||||
|
||||
boot_enable() {
|
||||
info "Setting the PC to boot into the desktop..."
|
||||
# The sync bridge (plasma-login-manager only; not there with SDDM) copies
|
||||
# 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
|
||||
# starts.
|
||||
sudo tee "$BOOT_UNIT" > /dev/null << EOF
|
||||
[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 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)."
|
||||
}
|
||||
|
||||
boot_disable() {
|
||||
info "Setting the PC to boot into gaming mode again..."
|
||||
sudo systemctl disable "$BOOT_UNIT_NAME" 2>/dev/null
|
||||
sudo rm -f "$BOOT_UNIT"
|
||||
sudo systemctl daemon-reload
|
||||
# Next boot into gamescope, as the conversion sets it up.
|
||||
sudo /usr/lib/steamos/steam-set-session gamescope-session.desktop
|
||||
[[ -x "$SESSION_SYNC" ]] && sudo "$SESSION_SYNC"
|
||||
ok "Boots into gaming mode from the next boot on."
|
||||
}
|
||||
|
||||
boot_choice() {
|
||||
# The menu row: the chosen option in brackets.
|
||||
if [[ "$1" == 1 ]]; then
|
||||
echo "Boot into: gamescope [desktop]"
|
||||
else
|
||||
echo "Boot into: [gamescope] desktop"
|
||||
fi
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
#!/bin/bash
|
||||
# "HDMI-CEC" menu item, on any PC: Valve's CEC daemon (cecd), TV/receiver
|
||||
# volume control (cec-audio-control) and the units that attach USB CEC
|
||||
# adapters (Pulse-Eight, RainShadow) with inputattach. They're only in
|
||||
# Valve's SteamOS (holo) repository, not in CachyOS or the AUR. Works with
|
||||
# any /dev/cec*: a GPU with CEC or a USB adapter. On a Steam Machine it also
|
||||
# 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.
|
||||
|
||||
CEC_PKGS=(cecd cec-audio-control inputattach-cec-units)
|
||||
# steamos-manager checks once, at its start, whether cecd runs; order it
|
||||
# after cecd so it never misses it at login.
|
||||
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
|
||||
|
||||
cec_link_steamos_manager() {
|
||||
# On a Steam Machine: steamos-manager writes cecd's config from Steam's
|
||||
# CEC settings (its configure-cecd unit, run before cecd), and only
|
||||
# offers Steam those settings when cecd was there at its start. Also
|
||||
# called by Steam Machine support, which installs steamos-manager after
|
||||
# this item ran.
|
||||
pacman -Q steamos-manager >/dev/null 2>&1 || return 0
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable steamos-manager-configure-cecd.service 2>/dev/null
|
||||
systemctl --user start steamos-manager-configure-cecd.service 2>/dev/null
|
||||
systemctl --user restart cecd.service 2>/dev/null
|
||||
systemctl --user restart steamos-manager.service 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
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.
|
||||
systemctl --user stop cecd.service 2>/dev/null
|
||||
sudo modprobe -r cros_ec_cec 2>/dev/null
|
||||
sudo modprobe cros_ec_cec 2>/dev/null
|
||||
systemctl --user 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)"
|
||||
info "Downloading Valve's Steam Machine CEC driver..."
|
||||
if ! curl -fsL "$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
|
||||
# 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"
|
||||
printf '%s\n' "# Written by Steamify: Valve's cros_ec_cec, which knows the Steam Machine." \
|
||||
"PACKAGE_NAME=\"$CEC_DKMS_NAME\"" "PACKAGE_VERSION=\"$CEC_DKMS_VER\"" \
|
||||
'BUILT_MODULE_NAME[0]="cros-ec-cec"' 'DEST_MODULE_LOCATION[0]="/updates/dkms"' \
|
||||
'AUTOINSTALL="yes"' >"$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 <dir> <package>: download the newest <package> from
|
||||
# Valve's newest holo repository into <dir> and print its path. The
|
||||
# SHA-256 comes from Valve's package index and is verified.
|
||||
local dir="$1" name="$2" repo desc file_name sha256
|
||||
repo="$(curl -fsL "$VALVE_MIRROR/" | grep -oE 'holo-[0-9]+\.[0-9]+/' | tr -d / | sort -V | tail -n 1)"
|
||||
if [[ ! -f "$dir/holo.db" ]]; then
|
||||
[[ -n "$repo" ]] && curl -fsL "$VALVE_MIRROR/$repo/os/x86_64/$repo.db" -o "$dir/holo.db" ||
|
||||
{ err "Couldn't read Valve's SteamOS package index ($VALVE_MIRROR)."; return 1; }
|
||||
fi
|
||||
desc="$(tar -tf "$dir/holo.db" 2>/dev/null | grep -E "^$name-[0-9][^/]*/desc$" | head -n 1)"
|
||||
[[ -n "$desc" ]] || { err "No $name in Valve's $repo repository."; return 1; }
|
||||
file_name="$(tar -xOf "$dir/holo.db" "$desc" | awk '/^%FILENAME%$/ { getline; print }')"
|
||||
sha256="$(tar -xOf "$dir/holo.db" "$desc" | awk '/^%SHA256SUM%$/ { getline; print }')"
|
||||
info "Downloading $file_name ($repo)..." >&2
|
||||
curl -fsL "$VALVE_MIRROR/$repo/os/x86_64/$file_name" -o "$dir/$file_name" ||
|
||||
{ err "Downloading $file_name failed."; return 1; }
|
||||
echo "$sha256 $dir/$file_name" | sha256sum -c --quiet - >&2 ||
|
||||
{ err "Checksum mismatch for $file_name; not using it."; return 1; }
|
||||
echo "$dir/$file_name"
|
||||
}
|
||||
|
||||
cec_enable() {
|
||||
local tmp p f files=()
|
||||
# inputattach, needed by inputattach-cec-units.
|
||||
if ! pacman -Q linuxconsole >/dev/null 2>&1; then
|
||||
sudo pacman -S --needed --noconfirm linuxconsole || { err "Installing linuxconsole failed."; return 1; }
|
||||
state_set cec installed_linuxconsole 1
|
||||
fi
|
||||
tmp="$(mktemp -d)"
|
||||
for p in "${CEC_PKGS[@]}"; do
|
||||
f="$(fetch_holo_pkg "$tmp" "$p")" || { rm -rf "$tmp"; return 1; }
|
||||
files+=("$f")
|
||||
done
|
||||
info "Installing Valve's CEC daemon..."
|
||||
if ! sudo pacman -U --needed --noconfirm "${files[@]}"; then
|
||||
rm -rf "$tmp"
|
||||
err "Installing ${CEC_PKGS[*]} failed."
|
||||
return 1
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
cec_driver_enable || return 1
|
||||
# Its udev rule gives the user /dev/cec*; cecd starts with the graphical
|
||||
# session, after steamos-manager wrote its config from Steam's settings.
|
||||
sudo udevadm control --reload
|
||||
sudo udevadm trigger --subsystem-match=cec --action=add
|
||||
sudo mkdir -p "$(dirname "$CEC_ORDER_DROPIN")"
|
||||
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
|
||||
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
|
||||
systemctl --user daemon-reload
|
||||
cec_link_steamos_manager
|
||||
if compgen -G "/dev/cec*" >/dev/null; then
|
||||
systemctl --user restart cecd.service 2>/dev/null
|
||||
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
|
||||
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*"
|
||||
fi
|
||||
}
|
||||
|
||||
cec_disable() {
|
||||
info "Removing HDMI-CEC..."
|
||||
systemctl --user 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
|
||||
sudo pacman -Rns --noconfirm "${CEC_PKGS[@]}" 2>/dev/null
|
||||
cec_driver_disable
|
||||
sudo rm -f "$CEC_ORDER_DROPIN" "$CEC_STEAM_DROPIN" "$CEC_STEAM_ENV"
|
||||
sudo rmdir "$(dirname "$CEC_ORDER_DROPIN")" "$(dirname "$CEC_STEAM_DROPIN")" "$(dirname "$CEC_STEAM_ENV")" 2>/dev/null
|
||||
systemctl --user daemon-reload
|
||||
if [[ -n "$(state_get cec installed_linuxconsole)" ]]; then
|
||||
sudo pacman -Rns --noconfirm linuxconsole 2>/dev/null
|
||||
state_clear cec
|
||||
fi
|
||||
sudo udevadm control --reload
|
||||
systemctl --user is-active -q steamos-manager.service 2>/dev/null &&
|
||||
systemctl --user restart steamos-manager.service
|
||||
ok "HDMI-CEC removed."
|
||||
}
|
||||
|
||||
cec_overview() {
|
||||
# For the menu's kernel overview.
|
||||
local dev="none"
|
||||
compgen -G "/dev/cec*" >/dev/null && dev="$(cd /dev && echo cec*)"
|
||||
echo "CEC devices: $dev cecd: $(systemctl --user is-active cecd 2>/dev/null)"
|
||||
}
|
||||
+5
-2
@@ -1,11 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Output helpers, prompts and small shared utilities.
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
c_reset="\033[0m"; c_bold="\033[1m"; c_green="\033[32m"; c_yellow="\033[33m"; c_red="\033[31m"; c_cyan="\033[36m"
|
||||
c_reset="\033[0m"; c_bold="\033[1m"; c_green="\033[32m"; c_yellow="\033[33m"; c_red="\033[31m"; c_cyan="\033[36m"; c_dim="\033[2m"
|
||||
|
||||
info() { echo -e "${c_cyan}[INFO]${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"; }
|
||||
warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; }
|
||||
err() { echo -e "${c_red}[ERROR] $*${c_reset}" >&2; }
|
||||
ask_yn() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# "Return to Gaming Mode" desktop shortcut and the sudoers rule it needs.
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
create_desktop_shortcut() {
|
||||
# Determine the home directory of the target user
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/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"
|
||||
printf '%s\n' "# Written by Steamify: the Steam Machine powers off instead of booting up again." \
|
||||
"PACKAGE_NAME=\"$POWEROFF_DKMS_NAME\"" "PACKAGE_VERSION=\"$POWEROFF_DKMS_VER\"" \
|
||||
"BUILT_MODULE_NAME[0]=\"$POWEROFF_DKMS_NAME\"" 'DEST_MODULE_LOCATION[0]="/updates/dkms"' \
|
||||
'AUTOINSTALL="yes"' >"$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."
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Booting into gaming mode: autologin into gamescope with SDDM or
|
||||
# plasma-login-manager (plus the session sync bridge the latter needs).
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
BASE_CONF="/etc/plasmalogin.conf"
|
||||
|
||||
|
||||
+199
-22
@@ -1,23 +1,124 @@
|
||||
#!/bin/bash
|
||||
# Interactive menu: detects which components are on, lets the user pick what
|
||||
# they want, then turns components on or off to match.
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
# Menu order. Components are turned on in this order and off in reverse;
|
||||
# gaming must come first (single user builds on it).
|
||||
COMPONENTS=(gaming theme glyphs single machine)
|
||||
COMPONENTS=(gaming boot theme glyphs single launcher vram cec machine poweroff kpin hdmi bios)
|
||||
# One-off actions rather than on/off components: never preselected, never
|
||||
# re-applied, not listed as on or off.
|
||||
ACTIONS=(bios)
|
||||
# Sub-options, shown indented under their parent and only while it's ticked.
|
||||
declare -A PARENT=([boot]=gaming [poweroff]=machine [kpin]=machine [hdmi]=machine [bios]=machine)
|
||||
# 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
|
||||
# upset other devices on the TV, even on SteamOS), except on a Steam Machine,
|
||||
# which has CEC like on SteamOS. HDMI refresh boost needs someone at the
|
||||
# screen to confirm each step.
|
||||
NO_PRESELECT=(boot cec kpin hdmi)
|
||||
# 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.1.0 [boot]=2.1.0 [theme]=2.1.0 [glyphs]=2.1.0 [single]=2.1.0
|
||||
[launcher]=2.1.0 [cec]=2.1.0 [machine]=2.2.0 [poweroff]=2.2.0 [vram]=2.3.0
|
||||
[kpin]=2.1.0 [hdmi]=2.1.0
|
||||
)
|
||||
|
||||
declare -A LABEL=(
|
||||
[gaming]="SteamOS conversion: boot into gaming mode, Steam on the desktop"
|
||||
[boot]="Boot into the desktop instead of gaming mode"
|
||||
[theme]="Install SteamOS theme: Vapor look (cachyos-vapor)"
|
||||
[glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode"
|
||||
[single]="Single user mode: no password, lock screen or log out (SDDM)"
|
||||
[launcher]="Steamify shortcut: the app on the desktop, Steamify Terminal in the launcher"
|
||||
[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)"
|
||||
[machine]="Steam Machine support: LED bar driver, hardware settings in Steam"
|
||||
[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"
|
||||
)
|
||||
declare -A CURRENT WANTED
|
||||
|
||||
component_available() {
|
||||
[[ "$1" != machine ]] || machine_available
|
||||
case "$1" in
|
||||
machine|poweroff) machine_available ;;
|
||||
vram) vram_available ;;
|
||||
kpin) kpin_available ;;
|
||||
hdmi) hdmi_available ;;
|
||||
bios) bios_available ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_action() { [[ " ${ACTIONS[*]} " == *" $1 "* ]]; }
|
||||
|
||||
boot_mode() {
|
||||
# boot_mode 0|1: the "Boot into" choice as a word, for messages; "turn
|
||||
# off boot into the desktop" reads as the opposite of what was chosen.
|
||||
if [[ "$1" == 1 ]]; then echo desktop; else echo gamescope; fi
|
||||
}
|
||||
|
||||
menu_visible() {
|
||||
# Shown in the menu. A sub-option ("Boot into", the kernel pin) only
|
||||
# while its parent is ticked.
|
||||
component_available "$1" || return 1
|
||||
[[ -z "${PARENT[$1]:-}" || "${WANTED[${PARENT[$1]}]:-0}" == 1 ]]
|
||||
}
|
||||
|
||||
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"; 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_new() {
|
||||
# A default 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 as its parent): never turned on or off.
|
||||
local p="${PARENT[$1]:-gaming}"
|
||||
[[ "$1" != gaming && "${CURRENT[$p]:-0}" == 1 && "${CURRENT[$1]:-0}" == 0 ]] &&
|
||||
! is_action "$1" && component_selectable "$1" && [[ " ${NO_PRESELECT[*]} " != *" $1 "* ]] &&
|
||||
[[ -z "$(state_get features "$1")" ]]
|
||||
}
|
||||
|
||||
component_selectable() {
|
||||
# Greyed out and not tickable when it has nothing to do.
|
||||
case "$1" in
|
||||
bios) bios_selectable ;;
|
||||
vram) vram_selectable ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_components() {
|
||||
@@ -27,20 +128,51 @@ detect_components() {
|
||||
if "${c}_status"; then CURRENT[$c]=1; any=true; else CURRENT[$c]=0; fi
|
||||
WANTED[$c]=${CURRENT[$c]}
|
||||
done
|
||||
# First run: preselect the full SteamOS experience.
|
||||
# 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.
|
||||
bios_available && { bios_lookup_newest; LABEL[bios]="$(bios_label)"; }
|
||||
# Greyed out with an NVIDIA card: say why.
|
||||
component_available vram && ! vram_selectable && LABEL[vram]="VRAM booster: not supported by NVIDIA's driver yet"
|
||||
# First run: preselect the full SteamOS experience (never an action).
|
||||
if [[ "$any" == false ]]; then
|
||||
for c in "${COMPONENTS[@]}"; do
|
||||
component_available "$c" && WANTED[$c]=1
|
||||
component_available "$c" && component_selectable "$c" && ! is_action "$c" &&
|
||||
[[ " ${NO_PRESELECT[*]} " != *" $c "* ]] && WANTED[$c]=1
|
||||
done
|
||||
machine_available && WANTED[cec]=1
|
||||
fi
|
||||
}
|
||||
|
||||
toggle_component() {
|
||||
local c="$1"
|
||||
component_selectable "$c" || return 1
|
||||
WANTED[$c]=$(( 1 - WANTED[$c] ))
|
||||
# Single user mode only makes sense on top of the SteamOS conversion.
|
||||
if [[ "$c" == single && "${WANTED[single]}" == 1 ]]; then WANTED[gaming]=1; fi
|
||||
if [[ "$c" == gaming && "${WANTED[gaming]}" == 0 ]]; then WANTED[single]=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.
|
||||
if [[ "$c" == boot && "${WANTED[boot]}" == 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" == bios && "${WANTED[bios]}" == 1 ]]; then WANTED[machine]=1; fi
|
||||
}
|
||||
|
||||
show_menu() {
|
||||
@@ -49,11 +181,28 @@ show_menu() {
|
||||
printf " ${c_bold}%-3s %-6s %-6s %s${c_reset}\n" "#" "Now" "Want" "Component"
|
||||
MENU_ITEMS=()
|
||||
for c in "${COMPONENTS[@]}"; do
|
||||
component_available "$c" || continue
|
||||
menu_visible "$c" || continue
|
||||
i=$((i + 1)); MENU_ITEMS[$i]=$c
|
||||
now="off"; [[ "${CURRENT[$c]}" == 1 ]] && now="${c_green}on${c_reset} "
|
||||
# Pad the plain word, then colour it: colour codes would count as width.
|
||||
now="off"; [[ "${CURRENT[$c]}" == 1 ]] && now="on"
|
||||
is_action "$c" && now="-"
|
||||
now="$(printf '%-6s' "$now")"
|
||||
[[ "${CURRENT[$c]}" == 1 ]] && now="${now/on/${c_green}on${c_reset}}"
|
||||
want="[ ]"; [[ "${WANTED[$c]}" == 1 ]] && want="[x]"
|
||||
printf " %-3s %-6b %-6s %s\n" "$i" "$now " "$want" "${LABEL[$c]}"
|
||||
if [[ "$c" == boot ]]; then
|
||||
# A choice rather than a checkbox: Now/Want show the mode.
|
||||
now="gaming"; [[ "${CURRENT[boot]}" == 1 ]] && now="desk"
|
||||
[[ "${CURRENT[gaming]}" == 1 ]] || now="-"
|
||||
want="gaming"; [[ "${WANTED[boot]}" == 1 ]] && want="desk"
|
||||
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
|
||||
printf " %-3s %b %-6s └ %b\n" "$i" "$now" "$want" "$(menu_label "$c")"
|
||||
else
|
||||
printf " %-3s %b %-6s %b\n" "$i" "$now" "$want" "$(menu_label "$c")"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
echo -e "$KERNEL_OVERVIEW"
|
||||
@@ -76,28 +225,46 @@ run_menu() {
|
||||
draw_menu_tui() {
|
||||
local cursor="$1" i=0 c box state line
|
||||
printf '\033[H\033[2J'
|
||||
echo -e "${c_bold}CachyOS Steam Deck-style Gamescope Boot Wizard${c_reset} v$VERSION"
|
||||
echo -e "${c_bold}Steamify CachyOS${c_reset} v$VERSION"
|
||||
echo "Pick what you want. Anything you untick is put back the way it was."
|
||||
echo
|
||||
MENU_ITEMS=()
|
||||
for c in "${COMPONENTS[@]}"; do
|
||||
component_available "$c" || continue
|
||||
menu_visible "$c" || continue
|
||||
MENU_ITEMS[$i]=$c
|
||||
box="[ ]"; [[ "${WANTED[$c]}" == 1 ]] && box="[${c_green}x${c_reset}]"
|
||||
state="off"; [[ "${CURRENT[$c]}" == 1 ]] && state="${c_green}on${c_reset}"
|
||||
state=" (now: off)"; [[ "${CURRENT[$c]}" == 1 ]] && state=" (now: ${c_green}on${c_reset})"
|
||||
is_action "$c" && state=" (opt-in, runs once)"
|
||||
line="$box ${LABEL[$c]}"
|
||||
if (( i == cursor )); then
|
||||
if [[ "$c" == boot ]]; then
|
||||
# A choice rather than a checkbox; Space switches it.
|
||||
# Indented under the conversion, whose sub-option it is.
|
||||
line="$(boot_choice "${WANTED[boot]}")"
|
||||
line=" └ ${line/\[/[${c_green}}"; line="${line/\]/${c_reset}]}"
|
||||
local mode=gamescope; [[ "${CURRENT[boot]}" == 1 ]] && mode=desktop
|
||||
state=" (${c_bold}←/→${c_reset} choose)"
|
||||
[[ "${CURRENT[gaming]}" == 1 ]] && state=" (now: $mode; ${c_bold}←/→${c_reset} choose)"
|
||||
elif [[ -n "${PARENT[$c]:-}" ]]; then
|
||||
line=" └ $line"
|
||||
fi
|
||||
if ! component_selectable "$c"; then
|
||||
# Greyed out: nothing to do (e.g. BIOS already up to date).
|
||||
local mark=" " tree=""; (( i == cursor )) && mark=" ${c_cyan}>${c_reset} "
|
||||
[[ -n "${PARENT[$c]:-}" ]] && tree=" └ "
|
||||
echo -e "${mark}${c_dim}${tree}[ ] ${LABEL[$c]} (not available)${c_reset}"
|
||||
elif (( i == cursor )); then
|
||||
# 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} (now: ${state})"
|
||||
echo -e " ${c_cyan}>${c_reset} ${c_bold}${line//"$c_reset"/"$c_reset$c_bold"}${c_reset}${state}"
|
||||
else
|
||||
echo -e " ${line} (now: ${state})"
|
||||
echo -e " ${line}${state}"
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo
|
||||
echo -e "$KERNEL_OVERVIEW"
|
||||
echo
|
||||
echo -e " ${c_bold}Up/Down${c_reset} move ${c_bold}Space${c_reset} select ${c_bold}Enter${c_reset} run ${c_bold}a${c_reset} run + re-apply what's on ${c_bold}q${c_reset} quit"
|
||||
echo -e " ${c_bold}Up/Down${c_reset} move ${c_bold}Space${c_reset} select ${c_bold}Left/Right${c_reset} choose ${c_bold}Enter${c_reset} run"
|
||||
echo -e " ${c_bold}a${c_reset} run + re-apply what's on ${c_bold}q${c_reset} quit"
|
||||
}
|
||||
|
||||
run_menu_tui() {
|
||||
@@ -107,6 +274,8 @@ run_menu_tui() {
|
||||
while true; do
|
||||
draw_menu_tui "$cursor"
|
||||
count=${#MENU_ITEMS[@]}
|
||||
# The list shrinks when the conversion (and its sub-option) is unticked.
|
||||
if (( cursor >= count )); then cursor=$(( count - 1 )); draw_menu_tui "$cursor"; fi
|
||||
IFS= read -rsn1 key
|
||||
if [[ "$key" == $'\e' ]]; then
|
||||
IFS= read -rsn2 -t 0.05 rest
|
||||
@@ -116,6 +285,9 @@ run_menu_tui() {
|
||||
$'\e[A'|k) (( cursor = (cursor + count - 1) % count )) ;;
|
||||
$'\e[B'|j) (( cursor = (cursor + 1) % count )) ;;
|
||||
" ") toggle_component "${MENU_ITEMS[$cursor]}" ;;
|
||||
# On the "Boot into" row: left = gamescope, right = desktop.
|
||||
$'\e[D'|h) [[ "${MENU_ITEMS[$cursor]}" == boot && "${WANTED[boot]}" == 1 ]] && toggle_component boot ;;
|
||||
$'\e[C'|l) [[ "${MENU_ITEMS[$cursor]}" == boot && "${WANTED[boot]}" == 0 ]] && toggle_component boot ;;
|
||||
"") break ;;
|
||||
a|A) REAPPLY=true; break ;;
|
||||
q|Q) tput cnorm 2>/dev/null; echo; return 1 ;;
|
||||
@@ -137,7 +309,8 @@ run_menu_lines() {
|
||||
q|Q) return 1 ;;
|
||||
*)
|
||||
if [[ "$reply" =~ ^[0-9]+$ && -n "${MENU_ITEMS[$reply]:-}" ]]; then
|
||||
toggle_component "${MENU_ITEMS[$reply]}"
|
||||
toggle_component "${MENU_ITEMS[$reply]}" ||
|
||||
warn "Not available: ${LABEL[${MENU_ITEMS[$reply]}]}"
|
||||
else
|
||||
warn "Unknown choice: $reply"
|
||||
fi
|
||||
@@ -160,7 +333,8 @@ plan_changes() {
|
||||
for c in "${COMPONENTS[@]}"; do
|
||||
component_available "$c" || continue
|
||||
[[ "${WANTED[$c]}" == 1 ]] || continue
|
||||
if [[ "${CURRENT[$c]}" == 0 || "$REAPPLY" == true ]] ||
|
||||
if is_action "$c"; then TO_ENABLE+=("$c"); continue; fi
|
||||
if [[ "${CURRENT[$c]}" == 0 || "$REAPPLY" == true ]] || feature_outdated "$c" ||
|
||||
[[ "$c" == gaming && "${CURRENT[single]}" != "${WANTED[single]}" ]]; then
|
||||
TO_ENABLE+=("$c")
|
||||
fi
|
||||
@@ -173,12 +347,15 @@ apply_changes() {
|
||||
[[ "${WANTED[single]}" == 1 ]] && LOGIN_MANAGER=sddm
|
||||
|
||||
for c in "${TO_DISABLE[@]}"; do
|
||||
echo; echo -e "${c_bold}Turning off: ${LABEL[$c]}${c_reset}"
|
||||
"${c}_disable" || failed+=("$c")
|
||||
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
|
||||
if "${c}_disable"; then feature_record "$c" disable; else failed+=("$c"); fi
|
||||
done
|
||||
for c in "${TO_ENABLE[@]}"; do
|
||||
echo; echo -e "${c_bold}Turning on: ${LABEL[$c]}${c_reset}"
|
||||
"${c}_enable" || failed+=("$c")
|
||||
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}"
|
||||
else echo; echo -e "${c_bold}Turning on: ${LABEL[$c]}${c_reset}"; fi
|
||||
if "${c}_enable"; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi
|
||||
done
|
||||
FAILED=("${failed[@]}")
|
||||
}
|
||||
+7
-2
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# Required packages and AUR helper handling.
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
aur_noninteractive_flags() {
|
||||
# Fully non-interactive install flags for the given AUR helper: no
|
||||
@@ -41,8 +41,13 @@ bootstrap_yay() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Build only, then install it ourselves: makepkg -i runs `sudo -k pacman`,
|
||||
# which forgets the cached sudo password and asks for it again.
|
||||
info "Building and installing yay (this runs makepkg as your user, not root)..."
|
||||
if (cd "$tmp_dir/yay-bin" && makepkg -si --noconfirm); then
|
||||
local -a pkgs
|
||||
if (cd "$tmp_dir/yay-bin" && makepkg --noconfirm) &&
|
||||
mapfile -t pkgs < <(cd "$tmp_dir/yay-bin" && makepkg --packagelist | grep -v -- '-debug-') &&
|
||||
sudo pacman -U --noconfirm "${pkgs[@]}"; then
|
||||
ok "yay installed."
|
||||
rm -rf "$tmp_dir"
|
||||
return 0
|
||||
|
||||
+84
-1
@@ -3,12 +3,91 @@
|
||||
# log out, since typing a password with a controller is miserable. Goes
|
||||
# together with SDDM (see lib/login-manager.sh). All changes are journaled
|
||||
# per user (lib/state.sh), so turning it off restores KDE's previous state.
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
single_status() {
|
||||
[[ "$(kreadconfig6 --file kdeglobals --group "KDE Action Restrictions" --key action/lock_screen)" == false ]]
|
||||
}
|
||||
|
||||
WALLET_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/kwalletd"
|
||||
# The user's own wallet while single user mode is on, and ours after.
|
||||
WALLET_BACKUP=".bak-steamify"
|
||||
WALLET_OURS=".steamify-single-user"
|
||||
|
||||
stop_kwallet() {
|
||||
# The wallet daemon keeps the file open and would write it back.
|
||||
local d
|
||||
for d in kwalletd6 ksecretd; do
|
||||
pkill -u "$USER" -x "$d" 2>/dev/null && sleep 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
single_wallet_enable() {
|
||||
# Like SteamOS: an empty, password-less wallet (Valve's own file), so
|
||||
# nothing asks for a wallet password (e.g. Brave at start): with
|
||||
# autologin nobody typed the login password that unlocks the usual one.
|
||||
# The user's wallet is moved aside and comes back when this is off. The
|
||||
# wallet from an earlier time in single user mode is reused, with
|
||||
# whatever was saved in it then.
|
||||
local tmp f
|
||||
if [[ -f "$WALLET_DIR/kdewallet.kwl$WALLET_OURS" ]]; then
|
||||
stop_kwallet
|
||||
for f in kdewallet.kwl kdewallet.salt; do
|
||||
[[ -f "$WALLET_DIR/$f" && ! -e "$WALLET_DIR/$f$WALLET_BACKUP" ]] &&
|
||||
mv "$WALLET_DIR/$f" "$WALLET_DIR/$f$WALLET_BACKUP"
|
||||
[[ -f "$WALLET_DIR/$f$WALLET_OURS" ]] && mv -f "$WALLET_DIR/$f$WALLET_OURS" "$WALLET_DIR/$f"
|
||||
done
|
||||
kset single kwalletrc Wallet "First Use" false
|
||||
ok "Single user mode's wallet (no password) is back; your own is kept as kdewallet.kwl$WALLET_BACKUP."
|
||||
return 0
|
||||
fi
|
||||
tmp="$(mktemp -d)"
|
||||
if ! fetch_valve_presets "$tmp"; then
|
||||
rm -rf "$tmp"
|
||||
warn "Couldn't get Valve's empty wallet; apps may still ask for the wallet password."
|
||||
return 0
|
||||
fi
|
||||
if [[ -f "$WALLET_DIR/kdewallet.kwl" ]] &&
|
||||
cmp -s "$WALLET_DIR/kdewallet.kwl" "$tmp/usr/share/kwalletd/kdewallet.kwl"; then
|
||||
rm -rf "$tmp"
|
||||
return 0
|
||||
fi
|
||||
stop_kwallet
|
||||
mkdir -p "$WALLET_DIR"
|
||||
for f in kdewallet.kwl kdewallet.salt; do
|
||||
# Never overwrite an earlier backup: that one is the user's.
|
||||
[[ -f "$WALLET_DIR/$f" && ! -e "$WALLET_DIR/$f$WALLET_BACKUP" ]] &&
|
||||
mv "$WALLET_DIR/$f" "$WALLET_DIR/$f$WALLET_BACKUP"
|
||||
install -m 600 "$tmp/usr/share/kwalletd/$f" "$WALLET_DIR/$f"
|
||||
done
|
||||
rm -rf "$tmp"
|
||||
kset single kwalletrc Wallet "First Use" false
|
||||
[[ -f "$WALLET_DIR/kdewallet.kwl$WALLET_BACKUP" ]] &&
|
||||
info "Your wallet is kept as $WALLET_DIR/kdewallet.kwl$WALLET_BACKUP and comes back when single user mode is off."
|
||||
ok "Empty wallet without a password: apps no longer ask for the wallet password."
|
||||
}
|
||||
|
||||
single_wallet_disable() {
|
||||
# The user's wallet back; ours (maybe with passwords saved since) is
|
||||
# kept next to it.
|
||||
[[ -f "$WALLET_DIR/kdewallet.kwl$WALLET_BACKUP" ]] || return 0
|
||||
stop_kwallet
|
||||
local f
|
||||
for f in kdewallet.kwl kdewallet.salt; do
|
||||
[[ -f "$WALLET_DIR/$f" ]] && mv -f "$WALLET_DIR/$f" "$WALLET_DIR/$f$WALLET_OURS"
|
||||
[[ -f "$WALLET_DIR/$f$WALLET_BACKUP" ]] && mv "$WALLET_DIR/$f$WALLET_BACKUP" "$WALLET_DIR/$f"
|
||||
done
|
||||
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() {
|
||||
# Launcher: only Sleep / Restart / Shut Down, no Session dropdown (where
|
||||
# Log Out lives). Restricting action/logout instead would also hide
|
||||
@@ -42,6 +121,8 @@ single_enable() {
|
||||
single_launcher
|
||||
|
||||
restart_plasmashell_if_stopped
|
||||
reload_screen_locker
|
||||
single_wallet_enable
|
||||
ok "Single user: no lock screen, user switching or log out."
|
||||
}
|
||||
|
||||
@@ -70,5 +151,7 @@ single_disable() {
|
||||
done
|
||||
fi
|
||||
restart_plasmashell_if_stopped
|
||||
reload_screen_locker
|
||||
single_wallet_disable
|
||||
ok "KDE's normal lock screen and user switching are back."
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
# off again exactly: the first time a component changes a key, the key's
|
||||
# previous value is recorded, and reverting the component puts it back (or
|
||||
# deletes the key if it didn't exist before).
|
||||
# Sourced by setup-gamescope-boot.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"
|
||||
UNSET=$'\x01'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Steam in the Plasma desktop session: gamepad UI args, virtual keyboard,
|
||||
# autostart. Part of the SteamOS conversion (gaming_enable/gaming_disable).
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
steam_enable() {
|
||||
info "Applying non-optional SteamOS environment & keyboard fixes..."
|
||||
|
||||
+199
-9
@@ -1,7 +1,8 @@
|
||||
#!/bin/bash
|
||||
# "Steam Machine support" menu item, only on Valve Fremont hardware: the
|
||||
# front LED bar driver, LED access for Steam, and steamos-manager.
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# 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.
|
||||
|
||||
detect_valve_fremont() {
|
||||
# Valve Steam Machine (Fremont) boards. The leds-valve driver's own DMI
|
||||
@@ -14,16 +15,182 @@ detect_valve_fremont() {
|
||||
[[ "$vendor" == "OEM" && "$product" == "F7F" ]]
|
||||
}
|
||||
|
||||
# Kernel pinned on a Steam Machine (optional): newer linux-cachyos releases
|
||||
# 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
|
||||
# through) needs no download.
|
||||
PINNED_KERNEL_VER="7.1.6-1"
|
||||
PINNED_KERNEL_KVER="7.1.6-1-cachyos"
|
||||
PINNED_KERNEL_PKGS=(linux-cachyos linux-cachyos-headers)
|
||||
PINNED_KERNEL_DIR="${PINNED_KERNEL_DIR:-/var/cache/steamify/kernel}"
|
||||
# SHA-256 of each package, so a file from any source is the one reviewed
|
||||
# here; its CachyOS signature is checked as well.
|
||||
declare -A PINNED_KERNEL_SHA256=(
|
||||
[linux-cachyos]=417fcd07102192b86e78e92ed7171d5a49378e9f57faea3fa2c7c541e9f68d08
|
||||
[linux-cachyos-headers]=d069866a11d9092746e1d5133cefd8924f027da2c9bf5af38d30b3aed6ded9bf
|
||||
)
|
||||
# Tried in order, after the kernel dir and pacman's cache. Our own release
|
||||
# always has these files; the archive keeps every release; the mirror only
|
||||
# the current one. PINNED_KERNEL_URL puts another source (a directory
|
||||
# holding the files) in front.
|
||||
PINNED_KERNEL_SOURCES=(
|
||||
${PINNED_KERNEL_URL:+"$PINNED_KERNEL_URL"}
|
||||
"https://github.com/theupriser/steamify-cachyos/releases/download/kernel-$PINNED_KERNEL_VER"
|
||||
"https://archive.cachyos.org/archive/cachyos"
|
||||
"https://mirror.cachyos.org/repo/x86_64/cachyos"
|
||||
)
|
||||
|
||||
pinned_kernel_installed() {
|
||||
local p
|
||||
for p in "${PINNED_KERNEL_PKGS[@]}"; do
|
||||
[[ "$(pacman -Q "$p" 2>/dev/null)" == "$p $PINNED_KERNEL_VER" ]] || return 1
|
||||
done
|
||||
}
|
||||
|
||||
fetch_pinned_kernel_file() {
|
||||
# $1 = file name. Local kernel dir first, then pacman's cache, then the
|
||||
# download sources. Downloads go to a .part file so a broken one is never
|
||||
# mistaken for a finished one.
|
||||
local f="$1" dest="$PINNED_KERNEL_DIR/$1" src
|
||||
[[ -s "$dest" ]] && return 0
|
||||
if [[ -s "/var/cache/pacman/pkg/$f" ]]; then
|
||||
sudo cp "/var/cache/pacman/pkg/$f" "$dest" && return 0
|
||||
fi
|
||||
for src in "${PINNED_KERNEL_SOURCES[@]}"; do
|
||||
if sudo curl -fL --retry 2 --connect-timeout 15 -o "$dest.part" "$src/$f" 2>/dev/null; then
|
||||
sudo mv "$dest.part" "$dest"
|
||||
return 0
|
||||
fi
|
||||
warn "Couldn't get $f from $src, trying the next source..."
|
||||
done
|
||||
sudo rm -f "$dest.part"
|
||||
return 1
|
||||
}
|
||||
|
||||
verify_pinned_kernel_pkg() {
|
||||
# $1 = package name, $2 = file. Both checks must pass: the SHA-256 from
|
||||
# this script, and the CachyOS signature (pacman on its own installs a
|
||||
# local file without a .sig: LocalFileSigLevel = Optional).
|
||||
local sum
|
||||
sum="$(sha256sum "$2" | cut -d' ' -f1)"
|
||||
if [[ "$sum" != "${PINNED_KERNEL_SHA256[$1]}" ]]; then
|
||||
err "$(basename "$2") doesn't match its expected checksum."
|
||||
return 1
|
||||
fi
|
||||
if ! sudo pacman-key --verify "$2.sig" "$2" >/dev/null 2>&1; then
|
||||
err "$(basename "$2") doesn't have a valid CachyOS signature."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
pin_kernel_in_pacman_conf() {
|
||||
# Adds our packages to IgnorePkg in [options], keeping what's there.
|
||||
local p
|
||||
for p in "${PINNED_KERNEL_PKGS[@]}"; do
|
||||
grep -Eq "^IgnorePkg\s*=.*(\s|=)$p(\s|$)" /etc/pacman.conf && continue
|
||||
if grep -Eq '^IgnorePkg\s*=' /etc/pacman.conf; then
|
||||
sudo sed -i -E "0,/^IgnorePkg\s*=/s/^(IgnorePkg\s*=.*)$/\1 $p/" /etc/pacman.conf
|
||||
else
|
||||
sudo sed -i -E "0,/^\[options\]/s//[options]\nIgnorePkg = $p/" /etc/pacman.conf
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
unpin_kernel_in_pacman_conf() {
|
||||
local p
|
||||
for p in "${PINNED_KERNEL_PKGS[@]}"; do
|
||||
sudo sed -i -E "/^IgnorePkg\s*=/s/\s$p(\s|$)/\1/" /etc/pacman.conf
|
||||
done
|
||||
# Drop the line if nothing is left on it.
|
||||
sudo sed -i -E '/^IgnorePkg\s*=\s*$/d' /etc/pacman.conf
|
||||
}
|
||||
|
||||
install_pinned_kernel() {
|
||||
sudo mkdir -p "$PINNED_KERNEL_DIR"
|
||||
pin_kernel_in_pacman_conf
|
||||
if pinned_kernel_installed; then
|
||||
ok "Kernel $PINNED_KERNEL_VER already installed and pinned."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local p f files=()
|
||||
info "Getting kernel $PINNED_KERNEL_VER (kept in $PINNED_KERNEL_DIR)..."
|
||||
for p in "${PINNED_KERNEL_PKGS[@]}"; do
|
||||
f="$p-$PINNED_KERNEL_VER-x86_64.pkg.tar.zst"
|
||||
fetch_pinned_kernel_file "$f" && fetch_pinned_kernel_file "$f.sig" ||
|
||||
{ err "Couldn't download $f from any source."; return 1; }
|
||||
if ! verify_pinned_kernel_pkg "$p" "$PINNED_KERNEL_DIR/$f"; then
|
||||
# Removed, so the next run fetches it again.
|
||||
sudo rm -f "$PINNED_KERNEL_DIR/$f" "$PINNED_KERNEL_DIR/$f.sig"
|
||||
err "Removed it; run the wizard again to download it once more."
|
||||
return 1
|
||||
fi
|
||||
files+=("$PINNED_KERNEL_DIR/$f")
|
||||
done
|
||||
|
||||
# pacman checks each package against the .sig next to it.
|
||||
info "Installing kernel $PINNED_KERNEL_VER..."
|
||||
if ! sudo pacman -U --noconfirm "${files[@]}"; then
|
||||
err "Installing kernel $PINNED_KERNEL_VER failed."
|
||||
return 1
|
||||
fi
|
||||
[[ "$(uname -r)" != "$PINNED_KERNEL_KVER" ]] && RESTART_FOR_LOGIN=true
|
||||
ok "Kernel $PINNED_KERNEL_VER installed and pinned (restart to use it)."
|
||||
}
|
||||
|
||||
remove_kernel_pin() {
|
||||
# Back to CachyOS's current kernel. The packages stay in
|
||||
# PINNED_KERNEL_DIR, so turning support on again doesn't download.
|
||||
unpin_kernel_in_pacman_conf
|
||||
pinned_kernel_installed || return 0
|
||||
info "Updating the kernel back to CachyOS's current version..."
|
||||
sudo pacman -Syu --noconfirm ||
|
||||
{ warn "Updating the kernel failed; run: sudo pacman -Syu"; return 0; }
|
||||
RESTART_FOR_LOGIN=true
|
||||
}
|
||||
|
||||
# "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() {
|
||||
pinned_kernel_installed && grep -Eq '^IgnorePkg\s*=.*\slinux-cachyos(\s|$)' /etc/pacman.conf
|
||||
}
|
||||
|
||||
kpin_enable() {
|
||||
install_pinned_kernel || return 1
|
||||
# The DKMS pacman hook built leds-valve for the new headers; make sure.
|
||||
if pacman -Qi leds-valve-dkms-git >/dev/null 2>&1 &&
|
||||
! dkms status -k "$PINNED_KERNEL_KVER" leds-valve-dkms 2>/dev/null | grep -q installed; then
|
||||
sudo dkms install leds-valve-dkms/0.1 -k "$PINNED_KERNEL_KVER" ||
|
||||
warn "Building the LED driver for $PINNED_KERNEL_KVER failed."
|
||||
fi
|
||||
}
|
||||
|
||||
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() {
|
||||
# DKMS can only build leds-valve against kernels whose headers are
|
||||
# installed. CachyOS ships several kernels (linux-cachyos, -lts, -bore,
|
||||
# ...); install the matching -headers package for every one present.
|
||||
local -a kernels headers=()
|
||||
mapfile -t kernels < <(pacman -Qqo /usr/lib/modules/*/pkgbase 2>/dev/null | sort -u)
|
||||
local k
|
||||
mapfile -t kernels < <(pacman -Qqo /usr/lib/modules/*/pkgbase 2>/dev/null | sort -u)
|
||||
# Installed headers are left alone: -S would update them past a pinned
|
||||
# kernel (linux-cachyos-headers on a Steam Machine).
|
||||
local have=0
|
||||
for k in "${kernels[@]}"; do
|
||||
pacman -Q "${k}-headers" >/dev/null 2>&1 && { have=$((have + 1)); continue; }
|
||||
pacman -Si "${k}-headers" >/dev/null 2>&1 && headers+=("${k}-headers")
|
||||
done
|
||||
if [[ ${#headers[@]} -eq 0 && $have -gt 0 ]]; then
|
||||
ok "Kernel headers already installed."
|
||||
return 0
|
||||
fi
|
||||
if [[ ${#headers[@]} -eq 0 ]]; then
|
||||
warn "Couldn't work out which kernel headers package you need."
|
||||
warn "Install the -headers package for your kernel (e.g. linux-cachyos-headers) yourself."
|
||||
@@ -56,8 +223,13 @@ install_valve_led_driver() {
|
||||
# got the running kernel's tree and failed (wrong kernel, and gcc against
|
||||
# a clang-built tree). DKMS reads this override after the package's
|
||||
# dkms.conf; written before the install so its own build works too.
|
||||
printf '%s\n' "# Written by cachyos-gamescope-boot: build for DKMS's target kernel." \
|
||||
'MAKE[0]="make KVERSION=${kernelver}"' | sudo tee "$LED_DKMS_OVERRIDE" >/dev/null
|
||||
# On a fresh system dkms isn't installed yet, so /etc/dkms doesn't exist.
|
||||
sudo mkdir -p "$(dirname "$LED_DKMS_OVERRIDE")"
|
||||
if ! printf '%s\n' "# Written by cachyos-gamescope-boot: build for DKMS's target kernel." \
|
||||
'MAKE[0]="make KVERSION=${kernelver}"' | sudo tee "$LED_DKMS_OVERRIDE" >/dev/null; then
|
||||
err "Couldn't write $LED_DKMS_OVERRIDE; without it the LED driver only builds for the running kernel."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! pacman -Qi leds-valve-dkms-git >/dev/null 2>&1; then
|
||||
info "Installing leds-valve-dkms-git from the AUR via $aur_helper (non-interactive)..."
|
||||
@@ -87,6 +259,13 @@ install_valve_led_driver() {
|
||||
# Load now, and on every boot.
|
||||
echo leds-valve | sudo tee /etc/modules-load.d/leds-valve.conf >/dev/null
|
||||
|
||||
# The running kernel was just replaced (the kernel pin): its modules are
|
||||
# gone, so the driver can only load after the restart.
|
||||
if [[ ! -d "/usr/lib/modules/$(uname -r)/kernel" ]]; then
|
||||
ok "The LED driver loads after the restart (into the new kernel)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Loading the leds-valve kernel module..."
|
||||
if sudo modprobe leds-valve; then
|
||||
ok "Module loaded."
|
||||
@@ -158,12 +337,14 @@ reload_powerdevil() {
|
||||
kernel_overview() {
|
||||
# Per installed kernel: headers (needed to build DKMS modules), 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.
|
||||
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
|
||||
local kdir k pkg headers hid led running line
|
||||
echo -e " ${c_bold}Kernels${c_reset} (> = running; controller = Steam controller driver, LEDs = LED bar driver built)"
|
||||
local kdir k pkg headers hid led off running line
|
||||
local legend="> = running; controller = Steam controller driver"
|
||||
[[ "$leds" == true ]] && legend+=", LEDs = LED bar driver built, power-off = power-off fix built"
|
||||
echo -e " ${c_bold}Kernels${c_reset} ($legend)"
|
||||
for kdir in /usr/lib/modules/*/; do
|
||||
k="$(basename "$kdir")"
|
||||
[[ -f "$kdir/pkgbase" ]] || continue
|
||||
@@ -174,10 +355,12 @@ kernel_overview() {
|
||||
line="$(printf '%-23s %-18s headers %b controller %b' "$k" "$pkg" "$headers" "$hid")"
|
||||
if [[ "$leds" == true ]]; then
|
||||
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
|
||||
echo -e " ${running}${line}"
|
||||
done
|
||||
cec_installed && { echo; echo -e " $(cec_overview)"; }
|
||||
if [[ "$leds" == true ]]; then
|
||||
local loaded="$miss" nodes
|
||||
lsmod | grep -q '^leds_valve' && loaded="$mark"
|
||||
@@ -194,6 +377,11 @@ machine_status() {
|
||||
}
|
||||
|
||||
machine_enable() {
|
||||
# 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.
|
||||
if [[ "${WANTED[kpin]:-0}" == 1 ]]; then
|
||||
install_pinned_kernel || { err "Couldn't set up kernel $PINNED_KERNEL_VER."; return 1; }
|
||||
fi
|
||||
ensure_aur_helper || { warn "Couldn't set up an AUR helper automatically. Install yay or paru, then run the wizard again."; return 1; }
|
||||
install_valve_led_driver || { warn "LED driver setup ran into a problem - see errors above."; return 1; }
|
||||
install_headers_boot_check
|
||||
@@ -216,6 +404,8 @@ EOF
|
||||
sudo systemctl enable --now inputplumber.service
|
||||
sudo systemctl enable --now steamos-manager.service
|
||||
systemctl --user enable steamos-manager.service 2>/dev/null
|
||||
# HDMI-CEC ran before this and couldn't link cecd to steamos-manager yet.
|
||||
cec_status && cec_link_steamos_manager
|
||||
|
||||
# Console-like power handling, as SteamOS's powerdevilrc (mains-power
|
||||
# part; there is no battery): the power button sleeps instead of showing
|
||||
|
||||
+3
-13
@@ -3,10 +3,10 @@
|
||||
# newest Valve steamdeck-kde-presets package: "Add to Steam" (file
|
||||
# right-click menu and launcher), Nested Desktop (Plasma as a game inside
|
||||
# gaming mode), the "Return to Gaming Mode" icon, the Steam Keyboard window
|
||||
# rule and an empty KWallet. Part of the SteamOS theme component
|
||||
# rule. Part of the SteamOS theme component
|
||||
# (lib/vapor-theme.sh). System files go to /usr/local (nothing
|
||||
# package-owned); per-user settings go through the undo journal.
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
VALVE_MIRROR="https://steamdeck-packages.steamos.cloud/archlinux-mirror"
|
||||
NESTED_DIR=/usr/local/share/applications/steam/holo-nested-desktop
|
||||
@@ -75,15 +75,6 @@ extras_enable() {
|
||||
sudo cp -r "$src/share/applications/steam/holo-nested-desktop" "$NESTED_DIR"
|
||||
sudo sed -i "s|/usr/share/applications/steam/holo-nested-desktop|$NESTED_DIR|" "$NESTED_DIR/holo-nested-desktop.desktop"
|
||||
|
||||
# An empty, password-less wallet (Valve's), so nothing asks for a wallet
|
||||
# password: with autologin nobody typed one to unlock it. Only when the
|
||||
# user has no wallet yet; an existing one is never touched.
|
||||
local wallet="${XDG_DATA_HOME:-$HOME/.local/share}/kwalletd"
|
||||
if [[ ! -f "$wallet/kdewallet.kwl" ]]; then
|
||||
mkdir -p "$wallet"
|
||||
install -m 600 "$src/share/kwalletd/kdewallet.kwl" "$src/share/kwalletd/kdewallet.salt" "$wallet/"
|
||||
kset theme kwalletrc Wallet "First Use" false
|
||||
fi
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
sudo gtk-update-icon-cache -q -f -t /usr/local/share/icons/hicolor 2>/dev/null || true
|
||||
@@ -122,7 +113,6 @@ extras_disable() {
|
||||
for pkg in kdialog zstd curl; do
|
||||
[[ -n "$(state_get theme "installed_$pkg")" ]] && sudo pacman -R --noconfirm "$pkg" >/dev/null 2>&1
|
||||
done
|
||||
# The keyboard rule and kwalletrc are reverted by the theme's journal.
|
||||
# The wallet itself stays: secrets may have been saved in it since.
|
||||
# The keyboard rule is reverted by the theme's journal.
|
||||
return 0
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# SteamOS Vapor desktop theme, from CachyOS's cachyos-vapor package.
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
VAPOR_LOOKANDFEEL=com.valve.vapor.desktop
|
||||
VAPOR_DEFAULTS=/usr/share/plasma/look-and-feel/$VAPOR_LOOKANDFEEL/contents/defaults
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/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 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))
|
||||
|
||||
vram_supported() {
|
||||
# WIZARD_VRAM_FAKE_NVIDIA=1 (tests): as with an NVIDIA card.
|
||||
[[ -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.
|
||||
awk -v min="$VRAM_MIN_BYTES" '$1 ~ /\/vram$/ && $2 >= min { found = 1 } END { exit !found }' \
|
||||
/sys/fs/cgroup/dmem.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
|
||||
}
|
||||
|
||||
# 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 &&
|
||||
systemctl --user 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; }
|
||||
systemctl --user 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 ]] &&
|
||||
systemctl --user start plasma-foreground-booster.service >/dev/null 2>&1
|
||||
ok "VRAM booster on: the game in front keeps its VRAM."
|
||||
}
|
||||
|
||||
vram_disable() {
|
||||
local pkgs
|
||||
systemctl --user 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."
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/bin/bash
|
||||
# "Steamify shortcut" menu item: "Steamify CachyOS" on the desktop and in the
|
||||
# app launcher opens the newest release of the app (curl | bash of
|
||||
# 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.
|
||||
|
||||
WIZARD_RELEASE="https://github.com/theupriser/steamify-cachyos/releases/latest/download"
|
||||
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).
|
||||
WIZARD_ICON="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps/cachyos-gamescope-boot-wizard.svg"
|
||||
WIZARD_LAUNCHER="${XDG_DATA_HOME:-$HOME/.local/share}/cachyos-gamescope-boot/run-wizard"
|
||||
WIZARD_APP_LAUNCHER="${XDG_DATA_HOME:-$HOME/.local/share}/cachyos-gamescope-boot/run-app"
|
||||
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_dir() {
|
||||
xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop"
|
||||
}
|
||||
|
||||
install_executable() {
|
||||
# install_executable <dest> <mode>: stdin to <dest>, with its mode set
|
||||
# before it appears. Plasma treats a desktop icon it saw while it wasn't
|
||||
# executable yet as untrusted and opens it in an editor.
|
||||
local dest="$1" tmp
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
tmp="$(mktemp "$(dirname "$dest")/.tmp.XXXXXX")"
|
||||
cat > "$tmp"
|
||||
chmod "$2" "$tmp"
|
||||
mv -f "$tmp" "$dest"
|
||||
}
|
||||
|
||||
launcher_status() {
|
||||
[[ -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() {
|
||||
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
|
||||
#!/bin/bash
|
||||
# Installed by Steamify CachyOS: run its newest release.
|
||||
# pipefail: a failed download must count as a failure, not an empty script.
|
||||
set -o pipefail
|
||||
if ! curl -fsSL --max-time 30 "$WIZARD_URL" | bash; then
|
||||
echo
|
||||
echo "Couldn't download or run the wizard. Are you connected to the internet?"
|
||||
echo
|
||||
read -rp "Press Enter to close this window... " _
|
||||
exit 1
|
||||
fi
|
||||
# Done: close the window after a countdown (Enter closes it right away).
|
||||
echo
|
||||
for (( i = 10; i > 0; i-- )); do
|
||||
printf '\rClosing this window in %2d seconds (Enter to close now)... ' "\$i"
|
||||
read -rs -t 1 _ && break
|
||||
done
|
||||
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
|
||||
|
||||
# The icon comes with the release; without it, Steam's own icon.
|
||||
local icon=steam tmp
|
||||
tmp="$(mktemp)"
|
||||
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"
|
||||
icon=cachyos-gamescope-boot-wizard
|
||||
gtk-update-icon-cache -q -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || true
|
||||
else
|
||||
warn "Couldn't download the shortcut's icon; using Steam's icon instead."
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
|
||||
local entry
|
||||
entry="[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Steamify CachyOS
|
||||
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
|
||||
Icon=$icon
|
||||
Terminal=false
|
||||
Categories=Game;System;Settings;" | install_executable "$WIZARD_TERMINAL_ENTRY" 644
|
||||
# The terminal-only shortcut from before 2.0.1.
|
||||
rm -f "$(wizard_desktop_dir)/$WIZARD_OLD_NAME" "$WIZARD_APPS/$WIZARD_OLD_NAME"
|
||||
kbuildsycoca6 >/dev/null 2>&1 || true
|
||||
ok "Steamify shortcut added: \"Steamify CachyOS\" (the app) on the desktop and in the launcher, \"Steamify Terminal\" in the launcher."
|
||||
}
|
||||
|
||||
launcher_disable() {
|
||||
# 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
|
||||
kbuildsycoca6 >/dev/null 2>&1 || true
|
||||
ok "Steamify shortcut removed."
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# 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>`; 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 |
|
||||
| `cros-ec-cec-single-port.patch` | `lib/cec.sh` (HDMI-CEC) | Makes Valve's Steam Machine CEC driver find amdgpu's HDMI port |
|
||||
@@ -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;
|
||||
@@ -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:*");
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# setup-gamescope-boot.sh
|
||||
#
|
||||
# Wizard that makes a CachyOS (KDE Plasma) install behave like SteamOS:
|
||||
# boot into a Steam Deck-style gamescope session, switch to the Plasma
|
||||
# desktop and back, and reset to gamescope on the next boot/logout.
|
||||
#
|
||||
# It opens with a menu that detects which components are on and turns
|
||||
# them on or off to match what the user picks; turning one off restores
|
||||
# what was there before (system files from .bak-gamescope-wizard backups,
|
||||
# KDE settings from the undo journal in lib/state.sh). Components live in
|
||||
# lib/, see README.md. Safe to re-run.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# Release version, see CHANGELOG.md.
|
||||
VERSION=0.7.0
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
for lib in common state packages login-manager single-user steam-desktop steam-machine vapor-theme steamos-extras desktop-shortcut menu; do
|
||||
# shellcheck source=/dev/null
|
||||
source "$SCRIPT_DIR/lib/$lib.sh"
|
||||
done
|
||||
|
||||
require_root_helper
|
||||
|
||||
echo -e "${c_bold}CachyOS Steam Deck-style Gamescope Boot Wizard${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."
|
||||
|
||||
if ! command -v pacman >/dev/null 2>&1; then
|
||||
err "This doesn't look like an Arch/CachyOS system (no pacman found). Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Everything per-user (autologin user, Steam, theme, shortcut) is for the
|
||||
# user running the script.
|
||||
TARGET_USER="$(id -un)"
|
||||
|
||||
detect_components
|
||||
run_menu || { info "Nothing changed."; exit 0; }
|
||||
plan_changes
|
||||
|
||||
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
|
||||
ok "Everything is already the way you want it."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "${c_bold}This will:${c_reset}"
|
||||
for c in "${TO_DISABLE[@]}"; do echo " - turn off: ${LABEL[$c]}"; done
|
||||
for c in "${TO_ENABLE[@]}"; do
|
||||
if [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}"; else echo " - turn on: ${LABEL[$c]}"; fi
|
||||
done
|
||||
ask_yn "Go ahead?" y || { info "Nothing changed."; exit 0; }
|
||||
|
||||
# Ask for the sudo password once and keep it fresh, instead of prompting at
|
||||
# random points during the run.
|
||||
sudo -n true 2>/dev/null || sudo -v || exit 1
|
||||
while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null &
|
||||
|
||||
apply_changes
|
||||
|
||||
echo
|
||||
detect_components
|
||||
echo -e "${c_bold}Done. Current state:${c_reset}"
|
||||
for c in "${COMPONENTS[@]}"; do
|
||||
component_available "$c" || continue
|
||||
if [[ "${CURRENT[$c]}" == 1 ]]; then echo -e " ${c_green}on ${c_reset} ${LABEL[$c]}"; else echo " off ${LABEL[$c]}"; fi
|
||||
done
|
||||
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
||||
warn "These had problems (see above): ${FAILED[*]}"
|
||||
fi
|
||||
echo
|
||||
|
||||
# Login manager changes only take effect after a restart.
|
||||
if [[ " ${TO_DISABLE[*]} ${TO_ENABLE[*]} " == *" gaming "* || " ${TO_DISABLE[*]} ${TO_ENABLE[*]} " == *" single "* ]]; then
|
||||
if ask_yn "Restart now so the changes take effect?" n; then
|
||||
sudo reboot
|
||||
else
|
||||
info "Restart whenever you're ready."
|
||||
fi
|
||||
fi
|
||||
Executable
+187
@@ -0,0 +1,187 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# steamify.sh - Steamify CachyOS
|
||||
#
|
||||
# Wizard that makes a CachyOS (KDE Plasma) install behave like SteamOS:
|
||||
# boot into a Steam Deck-style gamescope session, switch to the Plasma
|
||||
# desktop and back, and reset to gamescope on the next boot/logout.
|
||||
#
|
||||
# It opens with a menu that detects which components are on and turns
|
||||
# them on or off to match what the user picks; turning one off restores
|
||||
# what was there before (system files from .bak-gamescope-wizard backups,
|
||||
# KDE settings from the undo journal in lib/state.sh). Components live in
|
||||
# lib/, see TECHNICAL.md. Safe to re-run.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
# Release version, see CHANGELOG.md.
|
||||
VERSION=2.4.0
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu backend; do
|
||||
# shellcheck source=/dev/null
|
||||
source "$SCRIPT_DIR/lib/$lib.sh"
|
||||
done
|
||||
|
||||
require_root_helper
|
||||
|
||||
BACKEND=false
|
||||
[[ "${1:-}" == --backend ]] && BACKEND=true
|
||||
|
||||
if [[ "$BACKEND" == 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
|
||||
err "This doesn't look like an Arch/CachyOS system (no pacman found). Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Everything per-user (autologin user, Steam, theme, shortcut) is for the
|
||||
# user running the script.
|
||||
TARGET_USER="$(id -un)"
|
||||
|
||||
restart_needed() { [[ -n "${BIOS_NEEDS_RESTART:-}" || "$RESTART_FOR_LOGIN" == true ]]; }
|
||||
|
||||
restart_now() {
|
||||
if [[ -n "${BIOS_NEEDS_RESTART:-}" ]]; then
|
||||
warn "The BIOS update is written during this restart. Keep the power on and don't"
|
||||
warn "touch the machine until it has fully started again, even if the screen stays black."
|
||||
fi
|
||||
if [[ -n "${BIOS_DRY_RUN:-}" ]]; then
|
||||
ok "Dry run: would restart now (sudo reboot); not restarting."
|
||||
exit 0
|
||||
fi
|
||||
info "Restarting..."
|
||||
sudo reboot
|
||||
exit 0
|
||||
}
|
||||
|
||||
after_run() {
|
||||
# After a run: back to the menu, or restart right away when something
|
||||
# that just ran needs it. Returns 1 when input has ended (scripted runs).
|
||||
local reply
|
||||
if ! restart_needed; then
|
||||
read -rp "Press Enter to go back to the menu... " _ || return 1
|
||||
return 0
|
||||
fi
|
||||
if [[ -n "${BIOS_NEEDS_RESTART:-}" ]]; then
|
||||
warn "The BIOS update is installed at the next restart."
|
||||
else
|
||||
info "The login changes take effect after a restart."
|
||||
fi
|
||||
while true; do
|
||||
read -rp "$(echo -e "${c_bold}[m]${c_reset} back to the menu ${c_bold}[r]${c_reset} restart now: ")" reply || return 1
|
||||
case "$reply" in
|
||||
m|M|"") return 0 ;;
|
||||
r|R) restart_now ;;
|
||||
*) warn "Type m or r." ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
quit_prompt() {
|
||||
# On q: when something needs a restart, offer it (default yes); "n" goes
|
||||
# back to the menu, and the next q asks again. Returns 0 to show the menu
|
||||
# again, 1 to quit (nothing to restart, or input ended in scripted runs).
|
||||
local reply
|
||||
restart_needed || return 1
|
||||
echo
|
||||
if [[ -n "${BIOS_NEEDS_RESTART:-}" ]]; then
|
||||
warn "The BIOS update is written during the next restart. Keep the power on and"
|
||||
warn "don't touch the machine until it has fully started again, even if the screen stays black."
|
||||
else
|
||||
info "The changes take effect after a restart."
|
||||
fi
|
||||
read -rp "$(echo -e "${c_bold}Restart now?${c_reset} [Y/n] (n = back to the menu, Ctrl+C = quit without restarting) ")" reply || return 1
|
||||
case "$reply" in
|
||||
""|y|Y) restart_now ;;
|
||||
esac
|
||||
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
|
||||
|
||||
# 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.
|
||||
RESTART_FOR_LOGIN=false
|
||||
SUDO_KEEPALIVE=false
|
||||
while true; do
|
||||
detect_components
|
||||
if ! run_menu; then
|
||||
quit_prompt && continue
|
||||
break
|
||||
fi
|
||||
plan_changes
|
||||
|
||||
if [[ ${#TO_DISABLE[@]} -eq 0 && ${#TO_ENABLE[@]} -eq 0 ]]; then
|
||||
ok "Everything is already the way you want it."
|
||||
read -rp "Press Enter to go back to the menu... " _ || break
|
||||
continue
|
||||
fi
|
||||
|
||||
echo
|
||||
echo -e "${c_bold}This will:${c_reset}"
|
||||
for c in "${TO_DISABLE[@]}"; do
|
||||
if [[ "$c" == boot ]]; then echo " - boot into: gamescope (from the next boot)"
|
||||
else echo " - turn off: ${LABEL[$c]}"; fi
|
||||
done
|
||||
for c in "${TO_ENABLE[@]}"; do
|
||||
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 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
|
||||
ask_yn "Go ahead?" y || { info "Nothing changed."; continue; }
|
||||
|
||||
# Ask for the sudo password once and keep it fresh, instead of prompting
|
||||
# at random points during the run.
|
||||
sudo -n true 2>/dev/null || sudo -v || exit 1
|
||||
if [[ "$SUDO_KEEPALIVE" == false ]]; then
|
||||
SUDO_KEEPALIVE=true
|
||||
while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null &
|
||||
fi
|
||||
|
||||
apply_changes
|
||||
# Login manager changes only take effect after a restart.
|
||||
[[ " ${TO_DISABLE[*]} ${TO_ENABLE[*]} " =~ \ (gaming|single|boot|kpin)\ ]] &&
|
||||
RESTART_FOR_LOGIN=true
|
||||
|
||||
echo
|
||||
detect_components
|
||||
echo -e "${c_bold}Done. Current state:${c_reset}"
|
||||
for c in "${COMPONENTS[@]}"; do
|
||||
component_available "$c" && ! is_action "$c" || continue
|
||||
if [[ "$c" == boot ]]; then
|
||||
[[ "${CURRENT[gaming]}" == 1 ]] && echo " └ boots into: $(boot_mode "${CURRENT[boot]}")"
|
||||
elif [[ -n "${PARENT[$c]:-}" ]]; then
|
||||
[[ "${CURRENT[${PARENT[$c]}]}" == 1 ]] || continue
|
||||
if [[ "${CURRENT[$c]}" == 1 ]]; then echo -e " └ ${c_green}on ${c_reset} ${LABEL[$c]}"; else echo " └ off ${LABEL[$c]}"; fi
|
||||
elif [[ "${CURRENT[$c]}" == 1 ]]; then echo -e " ${c_green}on ${c_reset} ${LABEL[$c]}"; else echo " off ${LABEL[$c]}"; fi
|
||||
done
|
||||
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
||||
warn "These had problems (see above): ${FAILED[*]}"
|
||||
fi
|
||||
echo
|
||||
after_run || break
|
||||
done
|
||||
echo
|
||||
|
||||
if restart_needed; then
|
||||
# Only reached when input ended (scripted runs): never restart then.
|
||||
info "Not restarted; the changes take effect after a restart."
|
||||
else
|
||||
info "Bye."
|
||||
fi
|
||||
@@ -0,0 +1,265 @@
|
||||
// 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
|
||||
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 || []
|
||||
readonly property var rows: items.filter(function (i) { return !i.parent || want[i.parent]; })
|
||||
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).
|
||||
if (id === "gaming" && !w.gaming) w.single = false;
|
||||
if (id === "single" && w.single) 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;
|
||||
want = w;
|
||||
}
|
||||
function computePlan() {
|
||||
var p = [];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var it = items[i];
|
||||
if (it.kind !== "toggle") 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) { 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] && (!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);
|
||||
else if (a === "down") sel = Math.min(rows.length, 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);
|
||||
else if (a === "back") 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 = "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");
|
||||
else if (k === Qt.Key_Return || k === Qt.Key_Enter) act(Input.type === "remote" || screen !== "menu" ? "accept" : "apply");
|
||||
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 return;
|
||||
e.accepted = true;
|
||||
}
|
||||
function keyReleased(e) {
|
||||
if (screen === "bios2" && !e.isAutoRepeat && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) act("release");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// 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 }
|
||||
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
|
||||
Text { text: (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 } } }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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 ? 2 : 0; border.color: Theme.textHi
|
||||
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; cursorShape: Qt.PointingHandCursor; onClicked: btn.clicked() }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// The result of a run: all done, a problem, or nothing changed.
|
||||
import QtQuick
|
||||
|
||||
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(", ") + ". See the details." } }
|
||||
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() } }
|
||||
}
|
||||
}
|
||||
@@ -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" && 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 }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// The input in use (controller, keyboard or remote) and the button names
|
||||
// shown for it.
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
property string type: "keyboard"
|
||||
readonly property var glyphs: ({
|
||||
controller: { toggle: "A", choose: "◀ ▶", reapply: "Y", quit: "B", apply: "X", ok: "A", back: "B" },
|
||||
keyboard: { toggle: "Space", choose: "← →", reapply: "R", quit: "Esc", apply: "Enter", ok: "Enter", back: "Esc" },
|
||||
remote: { toggle: "OK", choose: "◀ ▶", reapply: "Red", quit: "Back", apply: "↓ Apply", ok: "OK", back: "Back" }
|
||||
})
|
||||
readonly property var g: glyphs[type]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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
|
||||
|
||||
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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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") } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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 {
|
||||
model: [[screen.g.toggle, "Toggle"], [screen.g.choose, "Choose"], [screen.g.reapply, "Re-apply what's on"], [screen.g.quit, "Quit"]]
|
||||
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; focusRing: app.sel === app.rows.length; height: 44; onClicked: app.goReview(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.", 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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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 } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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", "Steam starts silently on the desktop"] },
|
||||
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"] },
|
||||
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"] },
|
||||
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) 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: ({
|
||||
vram: { body: "Not available with your NVIDIA graphics card: NVIDIA's driver doesn't tell Linux how its video memory is used yet, so there's nothing to steer. It works with AMD and Intel graphics cards, and shows up here as soon as NVIDIA's driver supports it.",
|
||||
changes: ["Nothing: can't be turned on with NVIDIA's driver"] }
|
||||
})
|
||||
|
||||
// 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] || {};
|
||||
return item.selectable === false && unsupported[item.id] ? Object.assign({}, tx, unsupported[item.id]) : tx;
|
||||
}
|
||||
function label(id, fallback) { return (items[id] || {}).label || fallback || id; }
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# Steamify CachyOS - get and start the app (v2), 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/ and patches/) 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 (the
|
||||
# v2-ui-preview build's copy of this script sets it). 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..."
|
||||
setsid "$DEST/ui/steamify-ui" >/dev/null 2>&1 < /dev/null &
|
||||
Executable
+378
@@ -0,0 +1,378 @@
|
||||
#!/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
|
||||
|
||||
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,
|
||||
"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())
|
||||
Reference in new issue
Block a user