mirror of
https://github.com/theupriser/steamify-cachyos.git
synced 2026-10-03 17:41:58 +02:00
Compare commits
117
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
53f53a77ff | ||
|
|
6afef78721 | ||
|
|
1604ed10d4 | ||
|
|
8aab946c6f | ||
|
|
5cbe0079cb | ||
|
|
5c00504261 | ||
|
|
d7231ebbd8 | ||
|
|
eb3169efa1 | ||
|
|
10b51644c9 | ||
|
|
7802d90186 | ||
|
|
978f5a14cf | ||
|
|
a501df25b5 | ||
|
|
079c48f6fe | ||
|
|
ee5391c126 | ||
|
|
e741246098 | ||
|
|
9827bea8d5 | ||
|
|
dcc184ed39 |
No files matched your search
@@ -1,37 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Build a single-file version of the wizard for `curl | bash` use:
|
||||
# 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")/.."
|
||||
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.
|
||||
libs=$(grep -oP '^for lib in \K.*(?=; do$)' "$entry")
|
||||
[[ -n "$libs" ]] || { echo "bundle: can't find the lib source loop in $entry" >&2; exit 1; }
|
||||
|
||||
version="$(git describe --always --dirty 2>/dev/null || echo unknown)"
|
||||
version="v$(grep -oP '^VERSION=\K.*' "$entry"), $(git describe --always --dirty 2>/dev/null || echo unknown)"
|
||||
|
||||
{
|
||||
# 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 tools/bundle.sh - edit the"
|
||||
echo "# sources in the repository instead: https://github.com/theupriser/cachyos-gamescope-boot"
|
||||
echo "# Single-file build ($version), generated by .github/tools/bundle.sh - edit the"
|
||||
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,12 +1,22 @@
|
||||
# Builds the single-file wizard (tools/bundle.sh) and publishes it as the
|
||||
# asset of the rolling "latest" release, for:
|
||||
# curl -fsSL https://github.com/theupriser/cachyos-gamescope-boot/releases/download/latest/setup-gamescope-boot.sh | bash
|
||||
# Pull requests only build and check it.
|
||||
# Builds the single-file wizard (.github/tools/bundle.sh) and publishes it as a
|
||||
# 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/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:
|
||||
|
||||
@@ -22,33 +32,97 @@ jobs:
|
||||
fetch-depth: 0 # for the version in the bundle's header
|
||||
|
||||
- name: Build
|
||||
run: 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: Publish to the "latest" release
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
- name: Version
|
||||
id: version
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
notes="Single-file build of $(git describe --always) ($(git log -1 --format=%cs)).
|
||||
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 steamify.sh and add a CHANGELOG.md section to release this."
|
||||
else
|
||||
echo "released=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Publish the v${{ steps.version.outputs.version }} release
|
||||
if: >-
|
||||
github.event_name != 'pull_request' && github.ref == 'refs/heads/main' &&
|
||||
steps.version.outputs.released == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
# This version's section of CHANGELOG.md, without its heading.
|
||||
changes="$(awk -v v="$VERSION" '
|
||||
$0 ~ "^## " { if (found) exit; if (index($0, "## " v " ") == 1) { found = 1; next } }
|
||||
found' CHANGELOG.md)"
|
||||
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/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/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
|
||||
# is released, never for a push without a VERSION bump.
|
||||
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 "Steamify CachyOS - latest (v$VERSION)" --notes "$latest_notes" --latest=false
|
||||
else
|
||||
gh release create latest --title "Steamify CachyOS - latest (v$VERSION)" --notes "$latest_notes" --latest=false
|
||||
fi
|
||||
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/latest/setup-gamescope-boot.sh | bash"
|
||||
if gh release view latest >/dev/null 2>&1; then
|
||||
gh release edit latest --notes "$notes"
|
||||
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 latest --title "Latest single-file build" --notes "$notes" --latest
|
||||
gh release create v2-ui-preview --target "$GITHUB_SHA" --title "Steamify app (v2) preview" --notes "$notes" --prerelease --latest=false
|
||||
fi
|
||||
# Move the tag to this commit and replace the asset.
|
||||
git tag -f latest && git push -f origin latest
|
||||
gh release upload latest dist/setup-gamescope-boot.sh --clobber
|
||||
gh release upload v2-ui-preview dist/steamify-app.sh dist/steamify-app.tar.gz --clobber
|
||||
@@ -1 +1,2 @@
|
||||
dist/
|
||||
__pycache__/
|
||||
@@ -5,19 +5,23 @@ 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.
|
||||
- `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,18 +29,27 @@ 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
|
||||
`krevert <component>`. Never call `kwriteconfig6` directly for settings a
|
||||
component owns. System files are backed up with `backup_file` and restored
|
||||
on disable.
|
||||
- **Single-file build.** `tools/bundle.sh` inlines `lib/*.sh` in the order of
|
||||
- **Single-file build.** `.github/tools/bundle.sh` inlines `lib/*.sh` in the order of
|
||||
the entry point's `for lib in ...; do` source loop and wraps everything
|
||||
after that loop in `main()`. Keep that loop on one line, keep all logic in
|
||||
functions, and don't rely on `SCRIPT_DIR` for anything but sourcing.
|
||||
CI (`.github/workflows/bundle.yml`) publishes the bundle to the `latest`
|
||||
release on pushes to `main`.
|
||||
CI (`.github/workflows/bundle.yml`) publishes the bundle as release
|
||||
`v$VERSION` on pushes to `main`; an existing version is never overwritten,
|
||||
so bump `VERSION` for every release.
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -58,6 +71,22 @@ 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` (`steamify.sh`,
|
||||
shown in the menu header and the bundle header).
|
||||
- Every commit gets an entry in `CHANGELOG.md` under its version (short hash
|
||||
+ subject; a commit can't contain its own hash, so fill it in with the
|
||||
next commit). The section heading must be `## <VERSION> - <date>`: CI
|
||||
cuts the release notes out of the changelog by that heading.
|
||||
- Every PR that should be released bumps `VERSION` and adds its section.
|
||||
A push to `main` publishes release `v$VERSION` (tag + bundle + that
|
||||
changelog section) and marks it latest. An existing version is never
|
||||
overwritten: without a bump nothing is released, and pull requests show
|
||||
a warning.
|
||||
- Users install through `releases/latest/download/steamify.sh`
|
||||
(GitHub's newest release). The `latest` tag and release follow the newest
|
||||
version tag too (moved, asset replaced, when a new version is released),
|
||||
so the older URL `releases/download/latest/...` keeps working.
|
||||
|
||||
## Non-obvious behaviour to preserve
|
||||
|
||||
- `apply_changes` sets `$LOGIN_MANAGER` from the menu: `sddm` when single
|
||||
@@ -80,11 +109,21 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
||||
- The Steam desktop autostart unit is guarded with
|
||||
`ExecCondition=... XDG_CURRENT_DESKTOP = KDE`, so it doesn't start a
|
||||
second Steam inside gamescope.
|
||||
- Vapor theme: install Valve's files as shipped. Don't rewrite Valve's
|
||||
`metadata.json`; the icon theme is `breeze-dark` (there is no "Vapor"
|
||||
icon theme); wallpapers are flat JPGs in `usr/share/wallpapers`.
|
||||
`lookandfeeltool` does not switch the color scheme, so
|
||||
`plasma-apply-colorscheme Vapor` is applied explicitly.
|
||||
- Vapor theme: comes from the `cachyos-vapor` package (system-wide, in
|
||||
`/usr/share`); only remove it on disable if we installed it. It is applied
|
||||
with `lookandfeeltool --resetLayout` ("Desktop and window layout"), which
|
||||
replaces the panel/desktop layout: the layout files are backed up to
|
||||
`$STATE_DIR/theme-layout` and restored, and every key from Vapor's
|
||||
`contents/defaults` goes through `kset` first. `plasma-apply-colorscheme`
|
||||
runs with the `ColorScheme` key cleared, since it skips a scheme already
|
||||
named there. The layout reset drops single user's launcher settings, so
|
||||
`single_launcher` is re-applied.
|
||||
- SteamOS extras (`lib/steamos-extras.sh`, part of the theme): downloaded from
|
||||
Valve's newest `steamdeck-kde-presets` (repo db gives name + SHA-256) into
|
||||
`/usr/local`, never `/usr`. `gaming-return.svg` is a symlink in the package:
|
||||
install `steam-gaming-return.svg` under that name. The shortcut's icon is
|
||||
switched with `set_shortcut_icon`, since the conversion is created before
|
||||
the theme. An existing KWallet is never replaced or removed.
|
||||
- Plasma 6 has no separate systemtray containment: tray settings live on
|
||||
the systemtray applet itself (Valve's setup script targets Plasma 5).
|
||||
- Restart plasmashell via `systemctl --user` (`plasma-plasmashell.service`),
|
||||
@@ -97,19 +136,125 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
||||
wallpaper files only between `stop_plasmashell_for_edit` and
|
||||
`restart_plasmashell_if_stopped` (`lib/common.sh`).
|
||||
- LED driver: `leds-valve-dkms-git`'s Makefile builds against `uname -r`,
|
||||
so the script builds explicitly for the running kernel and installs
|
||||
headers for every installed kernel first. The module creates
|
||||
not DKMS's target kernel: `/etc/dkms/leds-valve-dkms.conf` sets
|
||||
`MAKE[0]="make KVERSION=${kernelver}"` (written before the AUR install).
|
||||
Without it, other kernels build against the running kernel's tree and
|
||||
fail (CachyOS kernels are clang-built; DKMS adds `LLVM=1` only for the
|
||||
target's tree). Headers for every installed kernel are installed first,
|
||||
and `ensure-kernel-headers.service` installs missing ones at boot (a
|
||||
pacman hook can't run pacman), which triggers DKMS's install hook. The module creates
|
||||
`/sys/class/leds/valve-leds*`.
|
||||
- `boot` ("Boot into: [gamescope] / desktop") is a choice row, not a
|
||||
checkbox: on = desktop. It's a sub-option of the conversion: `menu_visible`
|
||||
only shows it (indented, `└`) while `gaming` is ticked, so menu numbers
|
||||
after it shift by one when the conversion is unticked. It needs `gaming` (ticking it ticks gaming,
|
||||
unticking gaming unticks it) and is never preselected (`NO_PRESELECT`), so
|
||||
the conversion boots into gamescope by default. Desktop is
|
||||
`steamify-boot-desktop.service`, ordered `Before=` the login managers:
|
||||
`steam-set-session plasma.desktop` plus the plasmalogin sync bridge when it
|
||||
exists, at every boot. CachyOS's `cachyos-gamescope-autologin` still sets
|
||||
gamescope during each desktop session; the unit corrects it at boot.
|
||||
- `bios` is an *action* (`ACTIONS` in `lib/menu.sh`) and a sub-option of `machine`, not an on/off
|
||||
component: never preselected (not even on a first run), never re-applied
|
||||
by `a`, not listed in the state overview, and `bios_status` is always off.
|
||||
Only selectable when Valve's version differs from the installed one
|
||||
(`component_selectable`, greyed out otherwise). Download, SHA-256 and the
|
||||
fwupd device check (`get-details --json`: no `UpdateError`) come before the
|
||||
warnings. The warning box uses `█` for its frame: Konsole draws a long
|
||||
coloured row of `#` narrower, so the right edge wouldn't line up.
|
||||
Keep both confirmations (y/N, then typing `UPDATE`) and the warnings; the
|
||||
firmware comes from the newest `holo-X.Y` repo (`.files` db names the
|
||||
`.cab`, `.db` gives the SHA-256), and fwupd itself refuses non-Fremont
|
||||
hardware. In the VM, test it with `WIZARD_BIOS_DRY_RUN=1` (skips only the
|
||||
device check, never flashes) and a faked version (dev-env
|
||||
`BIOS_VERSION=F7F0107 ./run.sh --fremont`).
|
||||
- The entry point loops: menu, run, "back to the menu" (or `[m]`/`[r]` when a
|
||||
restart is needed), until `q`. When a restart is needed, `q` asks "Restart
|
||||
now? [Y/n]" (`quit_prompt`): `n` goes back to the menu, the next `q` asks
|
||||
again. Scripted input that runs out quits without restarting (never
|
||||
restart on EOF: `ask_yn` would take its default).
|
||||
- Steamify shortcut (`launcher`): "Steamify CachyOS" (desktop and launcher,
|
||||
id `steamify-ui.desktop`, the app's own id) runs `curl | bash` of
|
||||
`releases/latest/download/steamify-app.sh` without a terminal (in Konsole
|
||||
only while PySide6 is missing, for sudo); "Steamify Terminal" (launcher)
|
||||
runs `steamify.sh` in Konsole. Both always the newest release. The entry
|
||||
has `X-Steamify-Shortcut=true`: steamify-app.sh doesn't overwrite it, and
|
||||
disable only removes it then. A pre-2.0.1 shortcut (terminal only) is
|
||||
ticked by `launcher_repair`. Its icon, `assets/steam-gaming-settings.svg`
|
||||
(Valve's GPL-2.0 return icon with a gear), is a release asset too, and is
|
||||
downloaded from there (Steam's icon if that fails). Desktop files are
|
||||
written with their mode already set (`install_executable`): Plasma opens a
|
||||
desktop icon it first saw non-executable in an editor.
|
||||
The start script closes its window after a 10-second countdown on success
|
||||
and waits for Enter after an error; it uses `pipefail`, or a failed
|
||||
download would run an empty script and count as success.
|
||||
- HDMI refresh boost (`hdmi`, `lib/hdmi-refresh.sh`): a sub-option of
|
||||
`machine` (like `kpin` and `bios`), only on Fremont with the pinned kernel
|
||||
(stays visible while on); unticking `kpin` unticks it. It needs someone at
|
||||
the screen. Terminal: every step needs a "y" within 15 s
|
||||
(`WIZARD_HDMI_CONFIRM_SECONDS` for scripted tests). App: its own screen
|
||||
(`hdmi-options`, `hdmi-try`, `hdmi-reset` backend commands, the EDID cached
|
||||
in `$XDG_RUNTIME_DIR/steamify-hdmi`), then `apply --hdmi
|
||||
<output>=<w>x<h>:<rates>`; without `--hdmi` the backend refuses it. debugfs is root-only
|
||||
(glob it under sudo), and `edid_override` takes exactly `reset` with no
|
||||
newline. Build the EDID from the DDC read, not from sysfs: a live override
|
||||
replaces the kernel's copy. EDIDs are saved per display
|
||||
(`steamify-<id>.bin`, id = EDID bytes 8-17, listed in
|
||||
`/etc/steamify/hdmi-edid.conf`), never on the kernel command line (that
|
||||
applied to any display on the port): `steamify-edid-hotplug` (boot unit +
|
||||
udev drm hotplug rule) loads the connected display's file, else resets. It
|
||||
records what's loaded in `/run/steamify-edid` before its own
|
||||
`trigger_hotplug`, whose event runs it again. `hdmi_status` is on only
|
||||
while the connected display is boosted; disable forgets only that display
|
||||
(`kpin_disable` forgets all). The app manages the list (`hdmi-forget`,
|
||||
`hdmiDisplays` in status). Pre-2.1.0 setups are moved by `hdmi_migrate`.
|
||||
- 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
|
||||
@@ -118,5 +263,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.
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes, per version and per commit. Versions follow
|
||||
[Semantic Versioning](https://semver.org/); the current one is `VERSION` in
|
||||
`steamify.sh`. Versions before 0.7.0 were numbered afterwards,
|
||||
one per merged pull request.
|
||||
|
||||
## 2.3.0 - 2026-09-27
|
||||
|
||||
- **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
|
||||
Machine LED driver works on every installed kernel and survives kernel updates.
|
||||
|
||||
- `978f5a1` **feat: SteamOS theme from cachyos-vapor with its desktop layout, plus SteamOS desktop extras**
|
||||
- Installs `cachyos-vapor` instead of extracting Valve's `steamdeck-kde-presets`
|
||||
into `~/.local/share`; removes it on disable if the wizard installed it.
|
||||
- Applies the Vapor global theme with "Desktop and window layout": Steam Deck
|
||||
wallpaper, SteamOS panel and launcher icon. Layout files are backed up and
|
||||
restored; every key from Vapor's defaults goes through the undo journal.
|
||||
- Restores the real previous color scheme on disable (CachyOS's BreezeDark
|
||||
colors have no scheme name), instead of falling back to light Breeze.
|
||||
- Registers Vapor as the dark theme, so Plasma's Dark Mode toggle shows on.
|
||||
- Drops the extra SteamOS desktop defaults (GTK, Konsole, gsettings, panel tweaks).
|
||||
- New SteamOS desktop extras, downloaded from Valve's newest
|
||||
`steamdeck-kde-presets` (checksum verified) into `/usr/local`: Add to Steam,
|
||||
Nested Desktop, the SteamOS Return to Gaming Mode icon, the Steam Keyboard
|
||||
window rule and an empty KWallet (only when there is no wallet yet).
|
||||
- Single user mode's launcher settings are re-applied after the layout reset.
|
||||
- `7802d90` **fix: LED driver for every kernel, kernel headers at boot, console power button, kernel overview**
|
||||
- DKMS override (`/etc/dkms/leds-valve-dkms.conf`) so the LED driver builds
|
||||
for every installed kernel (the LTS kernel failed) and on kernel updates.
|
||||
- `ensure-kernel-headers.service` installs missing kernel headers at boot, so
|
||||
a newly added kernel gets the LED driver too.
|
||||
- Power button sleeps and no automatic suspend on mains power (Steam Machine).
|
||||
- Menu shows every kernel with its headers, Steam controller driver and LED
|
||||
driver, plus whether the LEDs are loaded.
|
||||
- The selected menu row is highlighted in full.
|
||||
- `10b5164` **docs: Version 0.7.0 and CHANGELOG**
|
||||
- `VERSION` in the entry point, shown in the menu header and the bundle.
|
||||
- README and AGENTS.md updated for the theme, the extras and the LED driver.
|
||||
- `eb3169e` **ci: Version and changelog in the release notes**
|
||||
- The `latest` release's title shows the version, and its notes include this
|
||||
version's section of `CHANGELOG.md`.
|
||||
- Fix a shellcheck warning in the bundle (a variable name shared with
|
||||
`lib/state.sh`), which would have failed the CI check.
|
||||
- `d7231eb` **chore: Move the bundle tool to .github/tools/bundle.sh**
|
||||
- `5c00504` **docs: Commit hashes in the changelog**
|
||||
- `8aab946` **ci: A release per version (tag v0.7.0, ...) instead of overwriting "latest"**
|
||||
- Each push to `main` publishes release `v$VERSION` with that version's
|
||||
changelog; an already released version is never overwritten (pull requests
|
||||
get a warning when `VERSION` wasn't bumped).
|
||||
- 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**
|
||||
- `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.
|
||||
|
||||
## 0.6.2 - 2026-09-23
|
||||
|
||||
- `a501df2` **Update steam-machine.sh**: install and enable `inputplumber`
|
||||
next to `steamos-manager`.
|
||||
|
||||
## 0.6.1 - 2026-09-23 (#5)
|
||||
|
||||
- `c0bd16e` **fix: Use curl | bash in the docs**: `bash <(...)` doesn't work in fish.
|
||||
|
||||
## 0.6.0 - 2026-09-23 (#4)
|
||||
|
||||
- `d654d44` **fix: Single-file build for curl, published by GitHub Actions**:
|
||||
`tools/bundle.sh` inlines `lib/*.sh`; CI publishes it to the `latest` release.
|
||||
|
||||
## 0.5.0 - 2026-09-23 (#3)
|
||||
|
||||
- `b91fa4b` **fix: Replace the question flow with a menu that turns components on and off**
|
||||
- `3d7f376` **fix: Interactive checkbox menu**: arrow keys to move, Space to select, Enter to run.
|
||||
|
||||
## 0.4.0 - 2026-09-23 (#2)
|
||||
|
||||
- `a0bc385` **fix: Add single user no password option with SDDM, fetch newest Vapor presets**
|
||||
|
||||
## 0.3.0 - 2026-09-23 (#1)
|
||||
|
||||
- `4ad6c2c` **fix: Fix setup bugs, add SteamOS desktop look and LED driver, split into modules**
|
||||
|
||||
## 0.2.0 - 2026-09-07
|
||||
|
||||
- `151a3bf` **add steam keyboard to desktop**
|
||||
- `1999b6e` **Automatically change theme**
|
||||
|
||||
## 0.1.0 - 2026-09-06
|
||||
|
||||
First version of the script.
|
||||
|
||||
- `16a7739`, `97ee6f3`, `6be1051` **commit the script**
|
||||
- `e1aee45`, `25bc8c0` **change return to gaming mode logo to original logo**
|
||||
+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** - Valve's own Vapor look for the desktop: wallpaper,
|
||||
dark mode and the SteamOS taskbar.
|
||||
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,314 +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/download/latest/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)
|
||||
> [x] Install SteamOS theme: Vapor look, dark mode, SteamOS taskbar (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)
|
||||
|
||||
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. 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
|
||||
|
||||
Downloads Valve's official `steamdeck-kde-presets` package from Valve's
|
||||
SteamOS mirror and installs it into `~/.local/share` - nothing system-wide:
|
||||
|
||||
- the Vapor global theme, color scheme, Plasma style, wallpapers and icons;
|
||||
- the Vapor GTK theme, Konsole profile and user avatars;
|
||||
- Valve's SteamOS desktop defaults, merged into your own config: Noto Sans
|
||||
fonts, no Xwayland input-permission prompt for Steam Input, the window
|
||||
rule that keeps the Steam keyboard on top and out of the taskbar, no
|
||||
welcome screen, light file indexing;
|
||||
- the SteamOS panel: solid (not floating), full width, 44 px high, tray icons
|
||||
scaled to fit, date below the time, and the SteamOS launcher icon;
|
||||
- dark mode for GTK, libadwaita and portal-aware apps (Firefox, Flatpaks).
|
||||
|
||||
Run inside a Plasma session, it applies everything live; otherwise it takes
|
||||
effect at the next desktop login.
|
||||
|
||||
**Package version.** The script always takes the newest
|
||||
`steamdeck-kde-presets` from the highest-numbered SteamOS release repository
|
||||
on the mirror (`jupiter-3.9`, `jupiter-3.10`, ...). That repository's pacman
|
||||
database gives the exact file name and SHA-256 checksum, which is verified
|
||||
after downloading. If the lookup fails, it falls back to the known-good
|
||||
3.9.4 (see `install_vapor_theme()` in `lib/vapor-theme.sh`). To check the
|
||||
current version yourself:
|
||||
|
||||
```bash
|
||||
m=https://steamdeck-packages.steamos.cloud/archlinux-mirror
|
||||
r=$(curl -fsL $m/ | grep -oE 'jupiter-[0-9]+\.[0-9]+/' | tr -d / | sort -V | tail -n 1)
|
||||
curl -fsL $m/$r/os/x86_64/$r.db | bsdtar -tf - | grep -o '^steamdeck-kde-presets-[^/]*' | sort -u
|
||||
```
|
||||
|
||||
### 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 the running kernel, 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;
|
||||
- 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, so after a kernel update, boot the new
|
||||
kernel and re-apply (`a` in the menu) or run
|
||||
`sudo dkms install leds-valve-dkms/0.1 -k "$(uname -r)"`.
|
||||
|
||||
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: Vapor, Valve's defaults, panel, dark mode |
|
||||
| `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 |
|
||||
| `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 `tools/bundle.sh`, checks the result with `bash -n` and
|
||||
shellcheck, and uploads it to the rolling `latest` release. 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 |
+230
@@ -0,0 +1,230 @@
|
||||
#!/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 apply ... --hdmi <output>=<w>x<h>:<rate>,... hdmi
|
||||
# HDMI refresh boost with the rates the app confirmed.
|
||||
# steamify.sh --backend hdmi-options
|
||||
# hdmi-options event: every HDMI output, its mode and rates to offer.
|
||||
# steamify.sh --backend hdmi-try <connector> <w> <h> <hz> [<rate>...]
|
||||
# steamify.sh --backend hdmi-reset <connector> <w> <h> <hz>
|
||||
# steamify.sh --backend hdmi-forget <display id>
|
||||
# removes a saved display (hdmi-forgotten event); status lists them.
|
||||
# switch to a rate live (hdmi-tried event), or back to the display's
|
||||
# own EDID (hdmi-reset event); the app asks in between.
|
||||
# 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
|
||||
# HDMI refresh boost's saved displays; "active" = connected and boosted.
|
||||
local hdmi_list="" active hid hname hmode hrates
|
||||
active=" $(hdmi_active_ids | tr '\n' ' ') "
|
||||
while IFS=$'\t' read -r hid hname hmode hrates; do
|
||||
[[ -n "$hid" ]] || continue
|
||||
hdmi_list+="${hdmi_list:+,}{\"id\":$(json_str "$hid"),\"name\":$(json_str "$hname"),\"mode\":$(json_str "$hmode"),\"rates\":$(json_str "$hrates"),\"active\":$([[ "$active" == *" $hid "* ]] && echo true || echo false)}"
|
||||
done < <(hdmi_saved)
|
||||
printf '{"version":%s,"firstRun":%s,"steamMachine":%s,"kernel":%s,"pinnedKernel":%s,"cecDevices":%s,"leds":%s,"bios":%s,"hdmiDisplays":[%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" "$hdmi_list" "$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 ;;
|
||||
--hdmi) HDMI_CHOICE+=("$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
|
||||
[[ "${WANTED[hdmi]:-0}" == 1 ]] && { WANTED[machine]=1; WANTED[kpin]=1; }
|
||||
[[ "${WANTED[kpin]:-0}" == 0 ]] && 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_hdmi() {
|
||||
# backend_hdmi options|try|reset|forget [args]: the app's HDMI screen. Events
|
||||
# only (no "finished"): the app goes on from its own screen.
|
||||
local what="$1" rc
|
||||
shift
|
||||
backend_sudo || return 1
|
||||
case "$what" in
|
||||
options)
|
||||
sudo pacman -S --needed --noconfirm i2c-tools python >/dev/null 2>&1
|
||||
backend_event hdmi-options "\"outputs\":$(hdmi_options 2>/dev/null)" ;;
|
||||
try)
|
||||
hdmi_try "$@" >/dev/null 2>&1; rc=$?
|
||||
backend_event hdmi-tried "\"hz\":$4,\"ok\":$([[ $rc -eq 0 ]] && echo true || echo false)" ;;
|
||||
reset)
|
||||
hdmi_reset "$@" >/dev/null 2>&1
|
||||
backend_event hdmi-reset ;;
|
||||
forget)
|
||||
[[ "${1:-}" =~ ^[0-9a-f]{20}$ ]] && hdmi_forget "$1" >/dev/null 2>&1; rc=$?
|
||||
backend_event hdmi-forgotten "\"id\":$(json_str "${1:-}"),\"ok\":$([[ $rc -eq 0 ]] && echo true || echo false)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
backend_main() {
|
||||
case "${1:-}" in
|
||||
status) backend_status ;;
|
||||
apply) shift; backend_apply "$@" ;;
|
||||
bios-prepare) backend_bios_prepare ;;
|
||||
bios-flash) backend_bios_flash ;;
|
||||
hdmi-options) backend_hdmi options ;;
|
||||
hdmi-try) shift; backend_hdmi try "$@" ;;
|
||||
hdmi-reset) shift; backend_hdmi reset "$@" ;;
|
||||
hdmi-forget) shift; backend_hdmi forget "$@" ;;
|
||||
*) 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() {
|
||||
|
||||
+12
-3
@@ -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
|
||||
@@ -43,9 +43,10 @@ EOF
|
||||
|
||||
# SteamOS itself uses Valve's "gaming-return" icon (Steam logo with a
|
||||
# return arrow; the Deck-style arrow is only used on Steam Deck
|
||||
# hardware). It comes with the Vapor theme; without it, use Steam's icon.
|
||||
# hardware). The theme installs it (lib/steamos-extras.sh) and updates
|
||||
# the shortcut via set_shortcut_icon; without it, use Steam's icon.
|
||||
local shortcut_icon="steam"
|
||||
[[ -f "$user_home/.local/share/icons/hicolor/scalable/actions/gaming-return.svg" ]] &&
|
||||
[[ -f /usr/local/share/icons/hicolor/scalable/actions/gaming-return.svg ]] &&
|
||||
shortcut_icon="gaming-return"
|
||||
|
||||
# 2. Generate the .desktop shortcut with instant session switcher strings
|
||||
@@ -69,6 +70,14 @@ EOF
|
||||
ok "Desktop shortcut created successfully."
|
||||
}
|
||||
|
||||
set_shortcut_icon() {
|
||||
# set_shortcut_icon <icon>: update an existing shortcut, if any.
|
||||
local shortcut
|
||||
shortcut="$(xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop")/Return to Gaming Mode.desktop"
|
||||
[[ -f "$shortcut" ]] && sed -i "s/^Icon=.*/Icon=$1/" "$shortcut"
|
||||
return 0
|
||||
}
|
||||
|
||||
remove_desktop_shortcut() {
|
||||
local desktop_dir
|
||||
desktop_dir="$(xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop")"
|
||||
|
||||
@@ -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,696 @@
|
||||
#!/bin/bash
|
||||
# "HDMI refresh boost" menu item, Steam Machine (Fremont) only: finds the
|
||||
# highest refresh rate the display runs at the desktop resolution over HDMI,
|
||||
# tests it live and saves an EDID override per display, loaded only while
|
||||
# that display is connected (see hdmi_install_hotplug). Two things keep displays below what they can do:
|
||||
# - Monitors put their fast modes in an extra EDID block (HDMI Forum EEODB);
|
||||
# the pinned kernel (7.1.6) only reads the first extension. Newer kernels
|
||||
# do, and do HDMI 2.1, so it's only offered with the pinned kernel.
|
||||
# - Their fastest modes need HDMI 2.1 (FRL), which amdgpu doesn't do on
|
||||
# every kernel; a mode with the display's shortest blanking at a slightly
|
||||
# lower rate often still fits HDMI 2.0's TMDS limit (600 MHz).
|
||||
# What a display claims isn't what it accepts: every step is shown and must
|
||||
# be confirmed, and no answer within 15 s switches back (the screen may be
|
||||
# black). 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; only removed now.
|
||||
HDMI_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-edid.conf
|
||||
# The saved displays, one per line: <id> TAB <name> TAB <w>x<h> TAB <rates>.
|
||||
# Their EDIDs are $HDMI_FW_DIR/steamify-<id>.bin.
|
||||
HDMI_MAP=/etc/steamify/hdmi-edid.conf
|
||||
# Per output, the ID of the display whose EDID is loaded, or "reset".
|
||||
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
|
||||
# No answer within this time switches back; longer for scripted tests.
|
||||
HDMI_CONFIRM_SECONDS="${WIZARD_HDMI_CONFIRM_SECONDS:-15}"
|
||||
# amdgpu's HDMI 2.0 limit; the display's own limit (from its EDID) may be lower.
|
||||
HDMI_TMDS_MAX_KHZ=600000
|
||||
# Rates picked and tested in the app ("<output>=<w>x<h>:<rate>,..."), set by
|
||||
# the backend's apply --hdmi; empty in the terminal menu, which tests itself.
|
||||
HDMI_CHOICE=()
|
||||
|
||||
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() {
|
||||
# No longer offered (it needed the pinned kernel): only shown while
|
||||
# anything of it is left, so a normal run removes it.
|
||||
detect_valve_fremont && hdmi_status
|
||||
}
|
||||
|
||||
hdmi_status() {
|
||||
# On while anything of it is left: saved displays (connected or not), the
|
||||
# hotplug script, an EDID file, or the kernel parameter of versions
|
||||
# before 2.1.0.
|
||||
[[ -n "$(hdmi_saved)" || -f "$HDMI_UNIT" || -n "$(hdmi_cmdline_param)" ]] ||
|
||||
compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null
|
||||
}
|
||||
|
||||
hdmi_saved() { [[ -f "$HDMI_MAP" ]] && grep -v '^#' "$HDMI_MAP"; }
|
||||
|
||||
hdmi_active_ids() {
|
||||
# IDs of the connected displays whose saved EDID is loaded.
|
||||
local f
|
||||
for f in "$HDMI_RUN"/*; do [[ -f "$f" ]] && grep -vx reset "$f"; done
|
||||
return 0
|
||||
}
|
||||
|
||||
hdmi_edid_tool() {
|
||||
# hdmi_edid_tool <command> ...: EDID parsing and building (python, as
|
||||
# it's byte work). Commands:
|
||||
# blocks <edid> number of blocks the display says it has
|
||||
# id <edid> vendor/product/serial, to recognise it
|
||||
# plan <edid> <w> <h> <cur_hz> "<limit_hz> <best> <step>..." for w x h
|
||||
# build <in> <out> <w> <h> <hz>... full EDID plus a DisplayID block
|
||||
# with those rates at the shortest blanking
|
||||
python3 - "$HDMI_TMDS_MAX_KHZ" "$@" <<'PY'
|
||||
import sys
|
||||
|
||||
src_khz = int(sys.argv[1]); cmd = sys.argv[2]; args = sys.argv[3:]
|
||||
def load(p): return bytearray(open(p, 'rb').read())
|
||||
def le(b): return int.from_bytes(b, 'little')
|
||||
|
||||
def cta_blocks(e):
|
||||
for i in range(128, len(e), 128):
|
||||
if e[i] == 0x02: yield i
|
||||
|
||||
def data_blocks(e, i):
|
||||
# CTA data block collection: (tag, extended tag, offset, length)
|
||||
end, p = e[i + 2], i + 4
|
||||
while p < i + end and p < i + 127:
|
||||
tag, n = e[p] >> 5, e[p] & 0x1f
|
||||
yield tag, (e[p + 1] if tag == 7 and n else None), p, n
|
||||
p += n + 1
|
||||
|
||||
def eeodb(e):
|
||||
for i in cta_blocks(e):
|
||||
for tag, ext, p, n in data_blocks(e, i):
|
||||
if tag == 7 and ext == 0x78: return p + 2
|
||||
return None
|
||||
|
||||
def nblocks(e):
|
||||
p = eeodb(e)
|
||||
return 1 + max(e[126], e[p] if p and p < len(e) else 0)
|
||||
|
||||
def timings(e):
|
||||
# (hz, clk_khz, ha, hb, hf, hs, hpos, va, vb, vf, vs, vpos)
|
||||
out = []
|
||||
def dtd(b):
|
||||
clk = le(b[0:2]) * 10
|
||||
if not clk: return
|
||||
ha = b[2] | (b[4] >> 4) << 8; hb = b[3] | (b[4] & 15) << 8
|
||||
va = b[5] | (b[7] >> 4) << 8; vb = b[6] | (b[7] & 15) << 8
|
||||
hf = b[8] | (b[11] >> 6 & 3) << 8; hs = b[9] | (b[11] >> 4 & 3) << 8
|
||||
vf = (b[10] >> 4) | (b[11] >> 2 & 3) << 4; vs = (b[10] & 15) | (b[11] & 3) << 4
|
||||
if b[17] & 0x80: return # interlaced
|
||||
out.append((clk * 1000 / ((ha + hb) * (va + vb)), clk, ha, hb, hf, hs, b[17] >> 1 & 1,
|
||||
va, vb, vf, vs, b[17] >> 2 & 1))
|
||||
for o in range(54, 126, 18): dtd(e[o:o + 18])
|
||||
for i in cta_blocks(e):
|
||||
d = e[i + 2]
|
||||
if d >= 4:
|
||||
for o in range(i + d, i + 127 - 17, 18): dtd(e[o:o + 18])
|
||||
for i in range(128, len(e), 128):
|
||||
if e[i] != 0x70: continue
|
||||
p, end = i + 5, i + 5 + e[i + 2]
|
||||
while p + 3 <= end:
|
||||
tag, n = e[p], e[p + 2]
|
||||
if tag == 0 and n == 0: break
|
||||
if tag == 0x03:
|
||||
for o in range(p + 3, p + 3 + n - 19, 20):
|
||||
b = e[o:o + 20]
|
||||
clk = (le(b[0:3]) + 1) * 10
|
||||
hf, vf = le(b[8:10]), le(b[16:18])
|
||||
ha, hb, va, vb = le(b[4:6]) + 1, le(b[6:8]) + 1, le(b[12:14]) + 1, le(b[14:16]) + 1
|
||||
out.append((clk * 1000 / ((ha + hb) * (va + vb)), clk, ha, hb, (hf & 0x7fff) + 1,
|
||||
le(b[10:12]) + 1, hf >> 15, va, vb, (vf & 0x7fff) + 1, le(b[18:20]) + 1, vf >> 15))
|
||||
p += 3 + n
|
||||
return out
|
||||
|
||||
def tmds_khz(e):
|
||||
# The display's own TMDS limit: HDMI Forum VSDB, else the HDMI 1.4 VSDB.
|
||||
hf = h14 = 0
|
||||
for i in cta_blocks(e):
|
||||
for tag, ext, p, n in data_blocks(e, i):
|
||||
if tag != 3 or n < 3: continue
|
||||
oui = e[p + 3] << 16 | e[p + 2] << 8 | e[p + 1]
|
||||
if oui == 0xC45DD8 and n >= 5: hf = e[p + 5] * 5000
|
||||
if oui == 0x000C03 and n >= 7: h14 = e[p + 7] * 5000
|
||||
lim = hf or h14 or 165000
|
||||
return min(lim, src_khz)
|
||||
|
||||
def max_vrefresh(e):
|
||||
# Range limits descriptor, the HDMI Forum / AMD VRR maximum.
|
||||
best = 0
|
||||
for o in range(54, 126, 18):
|
||||
b = e[o:o + 18]
|
||||
if b[0:3] == b'\0\0\0' and b[3] == 0xfd:
|
||||
best = max(best, b[6] + (255 if b[4] & 2 else 0))
|
||||
for i in cta_blocks(e):
|
||||
for tag, ext, p, n in data_blocks(e, i):
|
||||
if tag != 3 or n < 3: continue
|
||||
oui = e[p + 3] << 16 | e[p + 2] << 8 | e[p + 1]
|
||||
if oui == 0xC45DD8 and n >= 10: best = max(best, (e[p + 9] & 0xc0) << 2 | e[p + 10])
|
||||
if oui == 0x00001A and n >= 7: best = max(best, e[p + 7])
|
||||
return best
|
||||
|
||||
def shortest(e, w, h):
|
||||
ts = [t for t in timings(e) if t[2] == w and t[7] == h]
|
||||
return min(ts, key=lambda t: (t[2] + t[3]) * (t[7] + t[8])) if ts else None
|
||||
|
||||
if cmd == 'blocks':
|
||||
print(nblocks(load(args[0])))
|
||||
elif cmd == 'id':
|
||||
print(load(args[0])[8:18].hex())
|
||||
elif cmd == 'name':
|
||||
e = load(args[0])
|
||||
for o in range(54, 126, 18):
|
||||
if e[o:o + 3] == b'\0\0\0' and e[o + 3] == 0xfc:
|
||||
print(e[o + 5:o + 18].split(b'\n')[0].decode('ascii', 'replace').strip())
|
||||
elif cmd == 'plan':
|
||||
e = load(args[0]); w, h, cur = int(args[1]), int(args[2]), float(args[3])
|
||||
lim, t = tmds_khz(e), shortest(e, w, h)
|
||||
if not t: sys.exit(1)
|
||||
listed = [x[0] for x in timings(e) if x[2] == w and x[7] == h and x[1] <= lim]
|
||||
best = max(listed) if listed else cur
|
||||
top = int(lim * 1000 // ((t[2] + t[3]) * (t[7] + t[8])))
|
||||
vmax = max_vrefresh(e) or int(max(x[0] for x in timings(e) if x[2] == w and x[7] == h))
|
||||
top = min(top, vmax)
|
||||
# Not right at the limit: the top rounded down to ten, and the hundred
|
||||
# below it as a safe option. Rates the display already lists are left
|
||||
# out (its own timing is the safer one), and so is anything not faster
|
||||
# than what already works.
|
||||
steps = sorted({top // 10 * 10, top // 100 * 100})
|
||||
steps = [s for s in steps if s > best + 1 and not any(abs(s - x) < 1 for x in listed)]
|
||||
print(lim // 1000, round(best, 2), *steps)
|
||||
elif cmd == 'options':
|
||||
# JSON for the app: the display's name, the HDMI limit and the rates to
|
||||
# offer at w x h: its own modes the kernel skips ("monitor"), calculated
|
||||
# tens up to the limit ("calculated", ticked) and the exact limit when
|
||||
# that's no ten ("limit", not ticked).
|
||||
import json
|
||||
e, k = load(args[0]), load(args[1]); w, h, cur = int(args[2]), int(args[3]), float(args[4])
|
||||
lim, t = tmds_khz(e), shortest(e, w, h)
|
||||
name = ''
|
||||
for o in range(54, 126, 18):
|
||||
if e[o:o + 3] == b'\0\0\0' and e[o + 3] == 0xfc:
|
||||
name = e[o + 5:o + 18].split(b'\n')[0].decode('ascii', 'replace').strip()
|
||||
rates = []
|
||||
if t:
|
||||
fits = [x for x in timings(e) if x[2] == w and x[7] == h and x[1] <= lim]
|
||||
known = [x[0] for x in timings(k) if x[2] == w and x[7] == h]
|
||||
listed = [x[0] for x in fits]
|
||||
best = max(listed + [cur])
|
||||
for x in sorted(fits):
|
||||
if x[0] > cur + 1 and not any(abs(x[0] - y) < 0.5 for y in known):
|
||||
rates.append({'hz': round(x[0], 2), 'mhz': round(x[1] / 1000, 1), 'kind': 'monitor', 'pick': True})
|
||||
size = (t[2] + t[3]) * (t[7] + t[8])
|
||||
vmax = max_vrefresh(e) or int(max(listed + [cur]))
|
||||
top = min(int(lim * 1000 // size), vmax)
|
||||
tens = [r for r in range(10, top + 1, 10) if r > best + 1 and not any(abs(r - y) < 1 for y in listed)][-3:]
|
||||
for r in tens:
|
||||
rates.append({'hz': r, 'mhz': round(size * r / 1e6, 1), 'kind': 'calculated', 'pick': True})
|
||||
if top % 10 and top > best + 1:
|
||||
rates.append({'hz': top, 'mhz': round(size * top / 1e6, 1), 'kind': 'limit', 'pick': False})
|
||||
print(json.dumps({'name': name, 'limit': lim // 1000, 'rates': rates}))
|
||||
elif cmd == 'build':
|
||||
e = load(args[0]); w, h = int(args[2]), int(args[3]); rates = [int(r) for r in args[4:]]
|
||||
t = shortest(e, w, h)
|
||||
n = nblocks(e)
|
||||
e = e[:n * 128]
|
||||
out = b''
|
||||
for hz in rates:
|
||||
_, _, ha, hb, hf, hs, hp, va, vb, vf, vs, vp = t
|
||||
clk = round((ha + hb) * (va + vb) * hz / 1e4)
|
||||
x = (clk - 1).to_bytes(3, 'little') + bytes([0x08])
|
||||
for v, pol in ((ha, 0), (hb, 0), (hf, hp), (hs, 0), (va, 0), (vb, 0), (vf, vp), (vs, 0)):
|
||||
x += ((v - 1) | pol << 15).to_bytes(2, 'little')
|
||||
out += x
|
||||
if out:
|
||||
blk = bytearray(128)
|
||||
blk[0:5] = bytes([0x70, 0x12, 121, 0x00, 0x00])
|
||||
blk[5:8] = bytes([0x03, 0x00, len(out)])
|
||||
blk[8:8 + len(out)] = out
|
||||
blk[126] = (-sum(blk[1:126])) % 256
|
||||
blk[127] = (-sum(blk[:127])) % 256
|
||||
e += blk
|
||||
count = len(e) // 128 - 1
|
||||
e[126] = count
|
||||
e[127] = (-sum(e[:127])) % 256
|
||||
p = eeodb(e)
|
||||
if p:
|
||||
e[p] = count
|
||||
i = p - p % 128
|
||||
e[i + 127] = (-sum(e[i:i + 127])) % 256
|
||||
open(args[1], 'wb').write(e)
|
||||
PY
|
||||
}
|
||||
|
||||
hdmi_read_edid() {
|
||||
# hdmi_read_edid <card-connector> <out>: the display's complete EDID,
|
||||
# read over DDC block by block (the kernel's copy may miss blocks).
|
||||
local conn="$1" out="$2" bus n i tmp
|
||||
bus="$(basename "$(readlink -f "/sys/class/drm/$conn/ddc")")"; bus="${bus#i2c-}"
|
||||
tmp="$(mktemp)"
|
||||
cat "/sys/class/drm/$conn/edid" > "$out"
|
||||
if [[ -n "$bus" ]] && sudo modprobe i2c-dev 2>/dev/null; then
|
||||
n="$(hdmi_edid_tool blocks "$out")"
|
||||
: > "$tmp"
|
||||
for (( i = 0; i < n; i++ )); do
|
||||
# Segment pointer (0x30) selects each pair of blocks.
|
||||
sudo i2ctransfer -y "$bus" w1@0x30 $(( i / 2 )) w1@0x50 $(( (i % 2) * 128 )) r128@0x50 2>/dev/null |
|
||||
xxd -r -p >> "$tmp" 2>/dev/null ||
|
||||
{ [[ $i -lt 2 ]] && { sudo i2ctransfer -y "$bus" w1@0x50 $(( i * 128 )) r128@0x50 | xxd -r -p >> "$tmp"; }; } || break
|
||||
done
|
||||
# Keep the DDC copy only when it's whole and the same display as the
|
||||
# kernel's (not the whole block: a live override changes it).
|
||||
if [[ "$(stat -c %s "$tmp")" == $(( n * 128 )) &&
|
||||
"$(hdmi_edid_tool id "$tmp")" == "$(hdmi_edid_tool id "$out")" ]]; then
|
||||
cat "$tmp" > "$out"
|
||||
fi
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
hdmi_debugfs() {
|
||||
# debugfs directory of a connector (one of several paths to the same
|
||||
# GPU). Only root can list debugfs, so the glob runs under sudo.
|
||||
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" ]] && echo "$d"
|
||||
}
|
||||
|
||||
hdmi_override_live() {
|
||||
# hdmi_override_live <card-connector> <edid|reset>: swap the EDID the
|
||||
# kernel uses right now, without a restart.
|
||||
local d; d="$(hdmi_debugfs "$1")" || return 1
|
||||
# The kernel takes exactly "reset", without a newline.
|
||||
if [[ "$2" == reset ]]; then printf reset | sudo tee "$d/edid_override" >/dev/null
|
||||
else sudo tee "$d/edid_override" < "$2" >/dev/null; fi || return 1
|
||||
echo 1 | sudo tee "$d/trigger_hotplug" >/dev/null
|
||||
sleep 3
|
||||
}
|
||||
|
||||
hdmi_kscreen() {
|
||||
# hdmi_kscreen current|mode <output> [<w> <h> <hz>]: KDE's view of the
|
||||
# output: "w h hz" of the current mode, or set the mode closest to hz.
|
||||
kscreen-doctor -j 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
what, name = sys.argv[1], sys.argv[2]
|
||||
for o in json.load(sys.stdin)["outputs"]:
|
||||
if o["name"] != name: continue
|
||||
modes = {m["id"]: m for m in o["modes"]}
|
||||
if what == "current":
|
||||
m = modes[o["currentModeId"]]
|
||||
print(m["size"]["width"], m["size"]["height"], round(m["refreshRate"], 2))
|
||||
else:
|
||||
w, h, hz = int(sys.argv[3]), int(sys.argv[4]), float(sys.argv[5])
|
||||
ms = [m for m in o["modes"] if m["size"]["width"] == w and m["size"]["height"] == h]
|
||||
if ms: print(min(ms, key=lambda m: abs(m["refreshRate"] - hz))["id"])
|
||||
' "$@"
|
||||
}
|
||||
|
||||
hdmi_set_mode() {
|
||||
# hdmi_set_mode <output> <w> <h> <hz>: returns 1 if KDE has no such mode.
|
||||
local id rate
|
||||
id="$(hdmi_kscreen mode "$@")" || return 1
|
||||
[[ -n "$id" ]] || return 1
|
||||
kscreen-doctor "output.$1.mode.$id" >/dev/null 2>&1 || return 1
|
||||
sleep 2
|
||||
rate="$(hdmi_kscreen current "$1" | cut -d' ' -f3)"
|
||||
# Whole hertz: the rate asked for can be e.g. 59.97.
|
||||
[[ "${rate%.*}" -ge $(( ${4%.*} - 1 )) ]]
|
||||
}
|
||||
|
||||
hdmi_confirm() {
|
||||
# No answer (black screen, nobody there) is a no.
|
||||
local reply
|
||||
read -rt "$HDMI_CONFIRM_SECONDS" -p "$(echo -e "${c_bold}$1${c_reset} [y/N, ${HDMI_CONFIRM_SECONDS}s] ")" reply || { echo; return 1; }
|
||||
[[ "$reply" =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
hdmi_remove_boot_param() {
|
||||
# Removes the drm.edid_firmware= older versions put on the kernel command
|
||||
# line (it applied to whatever display was on that port), 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_write_map() {
|
||||
# hdmi_write_map <lines>: the saved displays list.
|
||||
sudo mkdir -p "$(dirname "$HDMI_MAP")"
|
||||
{ printf '# Written by steamify: HDMI refresh boost, one display per line:\n'
|
||||
printf '# <id>\t<name>\t<w>x<h>\t<rates>\n'
|
||||
[[ -n "$1" ]] && printf '%s\n' "$1"; } | sudo tee "$HDMI_MAP" >/dev/null
|
||||
}
|
||||
|
||||
hdmi_save() {
|
||||
# hdmi_save <edid> <w>x<h> <rates>: keep an EDID for the display it was
|
||||
# built from (block 0 is the display's own, so is its ID), replacing
|
||||
# what was saved for that display before.
|
||||
local id name
|
||||
id="$(hdmi_edid_tool id "$1")" && [[ -n "$id" ]] || return 1
|
||||
name="$(hdmi_edid_tool name "$1")"
|
||||
sudo install -Dm644 "$1" "$HDMI_FW_DIR/steamify-$id.bin" || return 1
|
||||
hdmi_write_map "$(hdmi_saved | awk -F'\t' -v i="$id" '$1 != i'
|
||||
printf '%s\t%s\t%s\t%s' "$id" "${name:-HDMI display}" "$2" "$3")"
|
||||
}
|
||||
|
||||
hdmi_forget() {
|
||||
# hdmi_forget <id>|all: remove saved displays; the hotplug script puts
|
||||
# a connected one back on its own EDID, and goes when none are left.
|
||||
local rest=""
|
||||
if [[ "$1" == all ]]; then
|
||||
sudo rm -f "$HDMI_FW_DIR"/steamify-*.bin
|
||||
else
|
||||
rest="$(hdmi_saved | awk -F'\t' -v i="$1" '$1 != i')"
|
||||
sudo rm -f "$HDMI_FW_DIR/steamify-$1.bin"
|
||||
fi
|
||||
if [[ -z "$rest" ]]; then hdmi_remove_hotplug; return; fi
|
||||
hdmi_write_map "$rest"
|
||||
sudo systemctl restart "$HDMI_UNIT_NAME"
|
||||
}
|
||||
|
||||
hdmi_migrate() {
|
||||
# Setups before 2.1.0: steamify-<output>.bin on the kernel command line,
|
||||
# the display's ID and mode in the state file. Saved per display now.
|
||||
local f out
|
||||
local -a smode
|
||||
for f in "$HDMI_FW_DIR"/steamify-*-*.bin; do
|
||||
[[ -e "$f" ]] || continue
|
||||
out="${f##*/steamify-}"; out="${out%.bin}"
|
||||
read -ra smode <<< "$(state_get hdmi "$out-mode")"
|
||||
[[ ${#smode[@]} -ge 2 ]] && hdmi_save "$f" "${smode[0]}x${smode[1]}" "${smode[*]:2}"
|
||||
sudo rm -f "$f"
|
||||
done
|
||||
state_clear hdmi
|
||||
hdmi_remove_boot_param
|
||||
}
|
||||
|
||||
hdmi_install_hotplug() {
|
||||
# A saved EDID is only loaded while its display is connected: a udev
|
||||
# rule runs the script at every hotplug (and a unit at boot, before the
|
||||
# login manager). The script reads the display's ID over DDC, which
|
||||
# shows the real display even while an override is loaded, and loads
|
||||
# that display's file or resets to its own EDID. Unplugging resets too,
|
||||
# so the next display never starts on the wrong one.
|
||||
sudo tee "$HDMI_HOTPLUG" > /dev/null << 'EOF'
|
||||
#!/bin/bash
|
||||
# Steamify CachyOS, HDMI refresh boost: load a display's saved EDID only
|
||||
# while it is connected. Runs as root (debugfs).
|
||||
FW=/usr/lib/firmware/edid RUN=/run/steamify-edid
|
||||
modprobe i2c-dev 2>/dev/null
|
||||
mkdir -p "$RUN"
|
||||
# Hotplug events come in bursts, and the link needs a moment before DDC works.
|
||||
sleep 1
|
||||
for c in /sys/class/drm/card*-HDMI-A-*; do
|
||||
[ -e "$c" ] || continue
|
||||
out="${c##*/}"; out="${out#card*-}"
|
||||
new=reset id=""
|
||||
if [ "$(cat "$c/status")" = connected ]; then
|
||||
bus="$(basename "$(readlink -f "$c/ddc")")"
|
||||
for i in 1 2 3 4 5; do
|
||||
# Bytes 8-17 of block 0: manufacturer, product, serial, date.
|
||||
id="$(i2ctransfer -y "${bus#i2c-}" w1@0x50 0 r18@0x50 2>/dev/null | tr -d ' ' | sed 's/0x//g')"
|
||||
[ ${#id} -eq 36 ] && break
|
||||
sleep 1
|
||||
done
|
||||
[ ${#id} -eq 36 ] && [ -f "$FW/steamify-${id:16}.bin" ] && new="${id:16}"
|
||||
fi
|
||||
[ "$(cat "$RUN/$out" 2>/dev/null || echo reset)" = "$new" ] && continue
|
||||
for d in /sys/kernel/debug/dri/*/"$out"; do [ -e "$d/edid_override" ] && break; done
|
||||
[ -e "$d/edid_override" ] || continue
|
||||
if [ "$new" = reset ]; then printf reset > "$d/edid_override"
|
||||
else cat "$FW/steamify-$new.bin" > "$d/edid_override"; fi
|
||||
# Before the hotplug below, whose own event runs this again.
|
||||
echo "$new" > "$RUN/$out"
|
||||
echo 1 > "$d/trigger_hotplug"
|
||||
done
|
||||
exit 0
|
||||
EOF
|
||||
sudo chmod 755 "$HDMI_HOTPLUG"
|
||||
# StartLimitIntervalSec=0: a burst of hotplugs must not get it rate-limited.
|
||||
sudo tee "$HDMI_UNIT" > /dev/null << EOF
|
||||
[Unit]
|
||||
Description=Steamify CachyOS: HDMI refresh boost EDID for the connected display
|
||||
StartLimitIntervalSec=0
|
||||
Wants=sys-kernel-debug.mount
|
||||
After=sys-kernel-debug.mount
|
||||
Before=display-manager.service sddm.service plasmalogin.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=$HDMI_HOTPLUG
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
||||
EOF
|
||||
# restart, not start: a hotplug during a run must still be looked at.
|
||||
printf '%s\n' '# Written by steamify: HDMI refresh boost, check the display at every hotplug.' \
|
||||
"ACTION==\"change\", SUBSYSTEM==\"drm\", ENV{HOTPLUG}==\"1\", RUN+=\"/usr/bin/systemctl --no-block restart $HDMI_UNIT_NAME\"" |
|
||||
sudo tee "$HDMI_UDEV_RULE" >/dev/null || return 1
|
||||
sudo systemctl daemon-reload
|
||||
sudo udevadm control --reload
|
||||
sudo systemctl enable "$HDMI_UNIT_NAME" >/dev/null 2>&1 || { err "Enabling $HDMI_UNIT_NAME failed."; return 1; }
|
||||
sudo systemctl restart "$HDMI_UNIT_NAME"
|
||||
}
|
||||
|
||||
hdmi_remove_hotplug() {
|
||||
local conn
|
||||
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_override_live "$conn" reset 2>/dev/null; done
|
||||
return 0
|
||||
}
|
||||
|
||||
hdmi_tune() {
|
||||
# hdmi_tune <card-connector> <tmpdir>: find and confirm the highest rate
|
||||
# at the desktop resolution. Sets HDMI_RESULT to "<w> <h> <hz>..." (the
|
||||
# confirmed new rates) or leaves it empty.
|
||||
local conn="$1" tmp="$2" out="${1#card*-}" cur w h hz plan lim best steps_str
|
||||
local -a steps ok_rates=()
|
||||
HDMI_RESULT=""
|
||||
# From the display's own EDID: a live one left by an earlier test would
|
||||
# make its rates look like what the display already does.
|
||||
hdmi_override_live "$conn" reset 2>/dev/null
|
||||
cur="$(hdmi_kscreen current "$out")"
|
||||
[[ -n "$cur" ]] || { warn "$out: KDE doesn't show this output; skipped."; return 0; }
|
||||
read -r w h hz <<< "$cur"
|
||||
info "$out: desktop resolution ${w}x${h} at ${hz} Hz. Reading the display's EDID..."
|
||||
hdmi_read_edid "$conn" "$tmp/$out.orig" || return 1
|
||||
if ! plan="$(hdmi_edid_tool plan "$tmp/$out.orig" "$w" "$h" "$hz")"; then
|
||||
warn "$out: the display lists no timing for ${w}x${h}; nothing to calculate."
|
||||
return 0
|
||||
fi
|
||||
read -r lim best steps_str <<< "$plan"
|
||||
read -ra steps <<< "${steps_str:-}"
|
||||
info "$out: HDMI limit ${lim} MHz, fastest mode that fits now: ${best} Hz."
|
||||
if [[ ${#steps[@]} -eq 0 ]]; then
|
||||
ok "$out: already at the highest rate that fits HDMI here; nothing to change."
|
||||
return 0
|
||||
fi
|
||||
info "$out: steps to try: ${steps[*]} Hz. After each one you're asked whether the"
|
||||
info "picture is OK; no answer within ${HDMI_CONFIRM_SECONDS} s (e.g. a black screen) switches back."
|
||||
ask_yn "Start the test?" y || return 0
|
||||
|
||||
hdmi_edid_tool build "$tmp/$out.orig" "$tmp/$out.test" "$w" "$h" "${steps[@]}" &&
|
||||
hdmi_override_live "$conn" "$tmp/$out.test" ||
|
||||
{ err "$out: couldn't load the test EDID (debugfs)."; return 1; }
|
||||
local s
|
||||
for s in "${steps[@]}"; do
|
||||
info "$out: switching to ${w}x${h} at $s Hz..."
|
||||
if hdmi_set_mode "$out" "$w" "$h" "$s" && hdmi_confirm "Is the picture OK at $s Hz?"; then
|
||||
ok "$out: $s Hz works."
|
||||
ok_rates+=("$s")
|
||||
else
|
||||
warn "$out: $s Hz doesn't work; stopping here."
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ ${#ok_rates[@]} -eq 0 ]]; then
|
||||
hdmi_override_live "$conn" reset
|
||||
hdmi_set_mode "$out" "$w" "$h" "$hz" >/dev/null
|
||||
info "$out: back to ${hz} Hz; nothing changed."
|
||||
return 0
|
||||
fi
|
||||
# Without the steps that failed, so KDE and gamescope never pick them.
|
||||
hdmi_edid_tool build "$tmp/$out.orig" "$tmp/$out.final" "$w" "$h" "${ok_rates[@]}" &&
|
||||
hdmi_override_live "$conn" "$tmp/$out.final"
|
||||
hdmi_set_mode "$out" "$w" "$h" "${ok_rates[-1]}" >/dev/null
|
||||
HDMI_RESULT="$w $h ${ok_rates[*]}"
|
||||
}
|
||||
|
||||
# --- For the app: options, live tries, and installing what was confirmed ---
|
||||
# The EDIDs read by hdmi_options, reused by the tries and the install.
|
||||
HDMI_CACHE="${XDG_RUNTIME_DIR:-/tmp}/steamify-hdmi"
|
||||
|
||||
hdmi_options() {
|
||||
# JSON array: per connected HDMI output its name, mode and rate options.
|
||||
local conn out cur w h hz opts sep=""
|
||||
mkdir -p "$HDMI_CACHE"
|
||||
printf '['
|
||||
for conn in $(hdmi_connectors); do
|
||||
out="${conn#card*-}"
|
||||
# A live EDID left by an earlier try would look like the display's own.
|
||||
hdmi_status || hdmi_override_live "$conn" reset 2>/dev/null
|
||||
cur="$(hdmi_kscreen current "$out")"
|
||||
[[ -n "$cur" ]] || continue
|
||||
read -r w h hz <<< "$cur"
|
||||
hdmi_read_edid "$conn" "$HDMI_CACHE/$out.orig" || continue
|
||||
opts="$(hdmi_edid_tool options "$HDMI_CACHE/$out.orig" "/sys/class/drm/$conn/edid" "$w" "$h" "$hz")" || continue
|
||||
# Every rate on offer loaded now, while the list is on screen: a try
|
||||
# is then only a mode switch (the current mode stays in the EDID).
|
||||
hdmi_status || hdmi_preload "$conn" "$w" "$h" "$opts"
|
||||
printf '%s{"connector":"%s","output":"%s","width":%s,"height":%s,"hz":%s,"info":%s}' "$sep" "$conn" "$out" "$w" "$h" "$hz" "$opts"
|
||||
sep=","
|
||||
done
|
||||
printf ']'
|
||||
}
|
||||
|
||||
hdmi_preload() {
|
||||
# hdmi_preload <card-connector> <w> <h> <options json>: load the EDID
|
||||
# with the calculated rates, as hdmi_try would on the first try.
|
||||
local conn="$1" out="${1#card*-}"
|
||||
local -a rates
|
||||
read -ra rates <<< "$(python3 -c 'import json, sys
|
||||
print(" ".join(str(r["hz"]) for r in json.loads(sys.argv[1])["rates"] if r["kind"] != "monitor"))' "$4")"
|
||||
hdmi_edid_tool build "$HDMI_CACHE/$out.orig" "$HDMI_CACHE/$out.test" "$2" "$3" "${rates[@]}" &&
|
||||
hdmi_override_live "$conn" "$HDMI_CACHE/$out.test" &&
|
||||
cp "$HDMI_CACHE/$out.test" "$HDMI_CACHE/$out.loaded"
|
||||
}
|
||||
|
||||
hdmi_try() {
|
||||
# hdmi_try <card-connector> <w> <h> <hz> [<rate>...]: switch to hz, with
|
||||
# an EDID holding the given calculated rates (loaded once per set).
|
||||
local conn="$1" w="$2" h="$3" hz="$4" out="${1#card*-}"
|
||||
shift 4
|
||||
[[ -f "$HDMI_CACHE/$out.orig" ]] || hdmi_read_edid "$conn" "$HDMI_CACHE/$out.orig"
|
||||
hdmi_edid_tool build "$HDMI_CACHE/$out.orig" "$HDMI_CACHE/$out.test" "$w" "$h" "$@" || return 1
|
||||
if ! cmp -s "$HDMI_CACHE/$out.test" "$HDMI_CACHE/$out.loaded" 2>/dev/null; then
|
||||
hdmi_override_live "$conn" "$HDMI_CACHE/$out.test" || return 1
|
||||
cp "$HDMI_CACHE/$out.test" "$HDMI_CACHE/$out.loaded"
|
||||
fi
|
||||
hdmi_set_mode "$out" "$w" "$h" "$hz"
|
||||
}
|
||||
|
||||
hdmi_reset() {
|
||||
# hdmi_reset <card-connector> <w> <h> <hz>: the display's own EDID again.
|
||||
hdmi_override_live "$1" reset
|
||||
rm -f "$HDMI_CACHE/${1#card*-}.loaded"
|
||||
hdmi_set_mode "${1#card*-}" "$2" "$3" "$4" >/dev/null
|
||||
}
|
||||
|
||||
hdmi_choose() {
|
||||
# For HDMI_CHOICE ("<output>=<w>x<h>:<rate>,..." from the app, rates it
|
||||
# confirmed): builds the final EDIDs in <tmpdir> like hdmi_tune does.
|
||||
local tmp="$1" item out mode rates w h
|
||||
local -a r
|
||||
for item in "${HDMI_CHOICE[@]}"; do
|
||||
out="${item%%=*}"; mode="${item#*=}"; rates="${mode#*:}"; mode="${mode%%:*}"
|
||||
w="${mode%x*}"; h="${mode#*x}"
|
||||
IFS=',' read -ra r <<< "$rates"
|
||||
HDMI_CHOSEN_CONN="$(basename /sys/class/drm/card*-"$out")"
|
||||
if [[ -f "$HDMI_CACHE/$out.orig" ]]; then cp "$HDMI_CACHE/$out.orig" "$tmp/$out.orig"
|
||||
else hdmi_read_edid "$HDMI_CHOSEN_CONN" "$tmp/$out.orig" || return 1; fi
|
||||
hdmi_edid_tool build "$tmp/$out.orig" "$tmp/$out.final" "$w" "$h" "${r[@]}" &&
|
||||
hdmi_override_live "$HDMI_CHOSEN_CONN" "$tmp/$out.final" || return 1
|
||||
[[ ${#r[@]} -gt 0 ]] && hdmi_set_mode "$out" "$w" "$h" "${r[-1]}" >/dev/null
|
||||
HDMI_RESULTS+=("$out|$w $h ${r[*]}")
|
||||
done
|
||||
}
|
||||
|
||||
hdmi_enable() {
|
||||
local -a HDMI_RESULTS=()
|
||||
if [[ -n "$(hdmi_cmdline_param)" ]]; then
|
||||
info "Moving the EDID override off the kernel command line, so it only applies to its display..."
|
||||
hdmi_migrate && hdmi_install_hotplug || { err "Moving it failed."; return 1; }
|
||||
ok "HDMI refresh boost now only applies to the display it was set up for."
|
||||
return 0
|
||||
fi
|
||||
if hdmi_status; then
|
||||
ok "Already set up for the connected display. Untick and tick it again to re-test."
|
||||
return 0
|
||||
fi
|
||||
if [[ "${BACKEND:-false}" == true && ${#HDMI_CHOICE[@]} -eq 0 ]]; then
|
||||
err "HDMI refresh boost needs you at the screen: pick and test the rates in the app first."
|
||||
return 1
|
||||
fi
|
||||
if [[ "${XDG_CURRENT_DESKTOP:-}" != *KDE* ]] || ! command -v kscreen-doctor >/dev/null; then
|
||||
err "Run this from the Plasma desktop: the test switches the display mode through KDE."
|
||||
return 1
|
||||
fi
|
||||
sudo pacman -S --needed --noconfirm i2c-tools python >/dev/null 2>&1 || { err "Installing i2c-tools failed."; return 1; }
|
||||
|
||||
local tmp conn out res saved=false rc=0
|
||||
local -a smode
|
||||
tmp="$(mktemp -d)"
|
||||
if [[ ${#HDMI_CHOICE[@]} -gt 0 ]]; then
|
||||
# Picked and tested in the app.
|
||||
hdmi_choose "$tmp" || rc=1
|
||||
else
|
||||
for conn in $(hdmi_connectors); do
|
||||
hdmi_tune "$conn" "$tmp" || { rc=1; continue; }
|
||||
[[ -n "$HDMI_RESULT" ]] && HDMI_RESULTS+=("${conn#card*-}|$HDMI_RESULT")
|
||||
done
|
||||
fi
|
||||
for res in "${HDMI_RESULTS[@]}"; do
|
||||
out="${res%%|*}"; read -ra smode <<< "${res#*|}"
|
||||
hdmi_save "$tmp/$out.final" "${smode[0]}x${smode[1]}" "${smode[*]:2}" && saved=true || rc=1
|
||||
done
|
||||
rm -rf "$tmp" "$HDMI_CACHE"
|
||||
[[ "$saved" == true ]] || return $rc
|
||||
if ! hdmi_install_hotplug; then
|
||||
# Nothing half-done left behind; the live EDID lasts until a restart.
|
||||
hdmi_remove_hotplug
|
||||
err "Making it permanent failed; the display is back on its own EDID."
|
||||
return 1
|
||||
fi
|
||||
ok "HDMI refresh boost is saved for this display; other displays keep their own settings."
|
||||
return $rc
|
||||
}
|
||||
|
||||
hdmi_disable() {
|
||||
# Everything, for every saved display: newer kernels read the whole EDID
|
||||
# themselves, and an old one loaded at the next hotplug would be wrong.
|
||||
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"
|
||||
|
||||
|
||||
+212
-23
@@ -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"
|
||||
[theme]="Install SteamOS theme: Vapor look, dark mode, SteamOS taskbar"
|
||||
[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,56 @@ 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; fi
|
||||
if [[ "$c" == kpin && "${WANTED[kpin]}" == 1 ]]; then WANTED[machine]=1; fi
|
||||
# HDMI refresh boost and the BIOS update sit under Steam Machine support;
|
||||
# the boost also needs the pinned kernel (newer kernels don't need it).
|
||||
if [[ "$c" == machine && "${WANTED[machine]}" == 0 ]]; then WANTED[hdmi]=0; WANTED[bios]=0; fi
|
||||
if [[ "$c" == kpin && "${WANTED[kpin]}" == 0 ]]; then WANTED[hdmi]=0; fi
|
||||
if [[ "$c" == hdmi && "${WANTED[hdmi]}" == 1 ]]; then WANTED[machine]=1; WANTED[kpin]=1; fi
|
||||
if [[ "$c" == bios && "${WANTED[bios]}" == 1 ]]; then WANTED[machine]=1; fi
|
||||
}
|
||||
|
||||
show_menu() {
|
||||
@@ -49,19 +186,40 @@ 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"
|
||||
echo
|
||||
}
|
||||
|
||||
run_menu() {
|
||||
# Sets REAPPLY. Returns 1 if the user quit. A checkbox list on a
|
||||
# terminal; a plain numbered prompt when input is piped (scripted runs).
|
||||
REAPPLY=false
|
||||
# Built once: the TUI redraws on every key press.
|
||||
KERNEL_OVERVIEW="$(kernel_overview)"
|
||||
if [[ -t 0 && -t 1 ]]; then
|
||||
run_menu_tui
|
||||
else
|
||||
@@ -72,25 +230,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}"
|
||||
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
|
||||
echo -e " ${c_cyan}>${c_reset} ${c_bold}${line}${c_reset} (now: ${state})"
|
||||
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}${state}"
|
||||
else
|
||||
echo -e " ${line} (now: ${state})"
|
||||
echo -e " ${line}${state}"
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
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 "$KERNEL_OVERVIEW"
|
||||
echo
|
||||
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() {
|
||||
@@ -100,6 +279,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
|
||||
@@ -109,6 +290,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 ;;
|
||||
@@ -130,7 +314,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
|
||||
@@ -153,7 +338,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
|
||||
@@ -166,12 +352,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
|
||||
|
||||
+99
-10
@@ -3,18 +3,110 @@
|
||||
# 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
|
||||
# Restart and Shut Down. Separate so the theme can re-apply it after
|
||||
# replacing the Plasma layout. Edit only while plasmashell is stopped.
|
||||
local applet grp
|
||||
for applet in $(plasma_applets org.kde.plasma.kickoff); do
|
||||
grp="Containments|${applet%%:*}|Applets|${applet#*:}|Configuration|General"
|
||||
kset single plasma-org.kde.plasma.desktop-appletsrc "$grp" primaryActions 3
|
||||
kset single plasma-org.kde.plasma.desktop-appletsrc "$grp" systemFavorites 'suspend,reboot,shutdown'
|
||||
done
|
||||
}
|
||||
|
||||
single_enable() {
|
||||
info "Turning off the lock screen, user switching and logging out..."
|
||||
stop_plasmashell_for_edit
|
||||
|
||||
# Hides Lock / Switch User in menus.
|
||||
local action applet
|
||||
local action
|
||||
for action in lock_screen switch_user start_new_session; do
|
||||
kset single kdeglobals "KDE Action Restrictions" "action/$action" false
|
||||
done
|
||||
@@ -26,16 +118,11 @@ single_enable() {
|
||||
# Ctrl+Alt+Del logout screen (format: current,default,description).
|
||||
kset single kglobalshortcutsrc ksmserver "Lock Session" $'none,Screensaver\tMeta+L,Lock Session'
|
||||
kset single kglobalshortcutsrc ksmserver "Log Out" 'none,Ctrl+Alt+Del,Show Logout Screen'
|
||||
# Launcher: only Sleep / Restart / Shut Down, no Session dropdown (where
|
||||
# Log Out lives). Restricting action/logout instead would also hide
|
||||
# Restart and Shut Down.
|
||||
for applet in $(plasma_applets org.kde.plasma.kickoff); do
|
||||
local grp="Containments|${applet%%:*}|Applets|${applet#*:}|Configuration|General"
|
||||
kset single plasma-org.kde.plasma.desktop-appletsrc "$grp" primaryActions 3
|
||||
kset single plasma-org.kde.plasma.desktop-appletsrc "$grp" systemFavorites 'suspend,reboot,shutdown'
|
||||
done
|
||||
single_launcher
|
||||
|
||||
restart_plasmashell_if_stopped
|
||||
reload_screen_locker
|
||||
single_wallet_enable
|
||||
ok "Single user: no lock screen, user switching or log out."
|
||||
}
|
||||
|
||||
@@ -64,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..."
|
||||
|
||||
+315
-18
@@ -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 are for the pinned kernel only (newer
|
||||
# ones read the whole EDID), also those of displays not connected now.
|
||||
[[ -n "$(hdmi_saved)" ]] && 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."
|
||||
@@ -51,6 +218,19 @@ install_valve_led_driver() {
|
||||
# silently builds nothing, so modprobe later finds no module.
|
||||
install_kernel_headers || return 1
|
||||
|
||||
# The upstream Makefile builds against `uname -r` instead of the kernel
|
||||
# DKMS targets, so every other installed kernel (e.g. linux-cachyos-lts)
|
||||
# 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.
|
||||
# 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)..."
|
||||
if ! "$aur_helper" -S "${flags[@]}" leds-valve-dkms-git; then
|
||||
@@ -61,23 +241,31 @@ install_valve_led_driver() {
|
||||
ok "leds-valve-dkms-git already installed."
|
||||
fi
|
||||
|
||||
# Build for the running kernel explicitly. The upstream Makefile uses
|
||||
# `uname -r` rather than DKMS's target kernel, so building for the
|
||||
# running kernel is the one case that reliably works.
|
||||
local kver
|
||||
kver="$(uname -r)"
|
||||
if ! dkms status -k "$kver" leds-valve-dkms 2>/dev/null | grep -q installed; then
|
||||
info "Building leds-valve for running kernel $kver..."
|
||||
# Build for every installed kernel that has headers, so booting another
|
||||
# one (e.g. the LTS kernel) still has the driver.
|
||||
local kdir kver
|
||||
for kdir in /usr/lib/modules/*/build; do
|
||||
[[ -d "$kdir" ]] || continue
|
||||
kver="$(basename "$(dirname "$kdir")")"
|
||||
dkms status -k "$kver" leds-valve-dkms 2>/dev/null | grep -q installed && continue
|
||||
info "Building leds-valve for kernel $kver..."
|
||||
if ! sudo dkms install leds-valve-dkms/0.1 -k "$kver"; then
|
||||
err "DKMS build failed. See the build log:"
|
||||
err "DKMS build for $kver failed. See the build log:"
|
||||
err " /var/lib/dkms/leds-valve-dkms/0.1/build/make.log"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# 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."
|
||||
@@ -96,6 +284,91 @@ install_valve_led_driver() {
|
||||
}
|
||||
|
||||
LED_UDEV_RULE="/etc/udev/rules.d/70-valve-leds-user.rules"
|
||||
LED_DKMS_OVERRIDE="/etc/dkms/leds-valve-dkms.conf"
|
||||
HEADERS_SCRIPT="/usr/local/lib/cachyos-gamescope-boot/ensure-kernel-headers"
|
||||
HEADERS_UNIT="/etc/systemd/system/ensure-kernel-headers.service"
|
||||
|
||||
install_headers_boot_check() {
|
||||
# DKMS's pacman hook rebuilds leds-valve for every kernel whose headers
|
||||
# get installed, but a newly added kernel (e.g. linux-cachyos-bore)
|
||||
# comes without them, and a pacman hook can't install packages itself.
|
||||
# So check at every boot and install missing headers, which triggers
|
||||
# that hook and builds the driver for the new kernel.
|
||||
sudo mkdir -p "$(dirname "$HEADERS_SCRIPT")"
|
||||
sudo tee "$HEADERS_SCRIPT" > /dev/null << 'EOF'
|
||||
#!/bin/bash
|
||||
# Installed by cachyos-gamescope-boot: install missing -headers for every
|
||||
# installed kernel, so DKMS builds leds-valve for it.
|
||||
set -u
|
||||
missing=()
|
||||
for k in $(pacman -Qqo /usr/lib/modules/*/pkgbase 2>/dev/null | sort -u); do
|
||||
pacman -Q "${k}-headers" >/dev/null 2>&1 && continue
|
||||
pacman -Si "${k}-headers" >/dev/null 2>&1 && missing+=("${k}-headers")
|
||||
done
|
||||
[[ ${#missing[@]} -eq 0 ]] && exit 0
|
||||
# Another pacman is running (e.g. an update): try again next boot.
|
||||
[[ -e /var/lib/pacman/db.lck ]] && { echo "pacman is busy, skipping"; exit 0; }
|
||||
echo "Installing missing kernel headers: ${missing[*]}"
|
||||
exec pacman -S --needed --noconfirm "${missing[@]}"
|
||||
EOF
|
||||
sudo chmod 755 "$HEADERS_SCRIPT"
|
||||
sudo tee "$HEADERS_UNIT" > /dev/null << EOF
|
||||
[Unit]
|
||||
Description=Install missing kernel headers so DKMS builds leds-valve for every kernel
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=$HEADERS_SCRIPT
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable ensure-kernel-headers.service
|
||||
}
|
||||
|
||||
reload_powerdevil() {
|
||||
qdbus6 org.kde.Solid.PowerManagement /org/kde/Solid/PowerManagement \
|
||||
org.kde.Solid.PowerManagement.refreshStatus >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
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 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 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
|
||||
pkg="$(< "$kdir/pkgbase")"
|
||||
headers="$miss"; [[ -d "$kdir/build" ]] && headers="$mark"
|
||||
hid="$miss"; compgen -G "$kdir/kernel/drivers/hid/hid-steam.ko*" >/dev/null && hid="$mark"
|
||||
running=" "; [[ "$k" == "$(uname -r)" ]] && running="${c_cyan}>${c_reset} "
|
||||
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"
|
||||
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"
|
||||
nodes="$(compgen -G '/sys/class/leds/valve-leds*' | wc -l)"
|
||||
echo
|
||||
echo -e " LED driver loaded: ${loaded} LED nodes: ${nodes} steamos-manager: $(systemctl is-active steamos-manager 2>/dev/null)"
|
||||
fi
|
||||
}
|
||||
|
||||
machine_available() { detect_valve_fremont; }
|
||||
|
||||
@@ -104,8 +377,14 @@ 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
|
||||
|
||||
# The LED files are root-only. Steam runs as the user; let it write them
|
||||
# in case it drives the bar directly (Valve's own privileged-write helper
|
||||
@@ -118,13 +397,24 @@ EOF
|
||||
sudo udevadm control --reload
|
||||
sudo udevadm trigger --subsystem-match=leds --action=add
|
||||
|
||||
# steamos-manager is what Steam in gaming mode talks to for hardware
|
||||
# settings (fan, HDMI-CEC TV control, performance); it knows the Steam
|
||||
# Machine from its DMI data.
|
||||
info "Installing steamos-manager (Steam Machine hardware settings in Steam)..."
|
||||
sudo pacman -S --needed --noconfirm steamos-manager || { err "Installing steamos-manager failed."; return 1; }
|
||||
# steamos-manager and inputplumber are what Steam in gaming mode talks to
|
||||
# for hardware and controller settings; it knows the Steam Machine from its DMI data.
|
||||
info "Installing hardware manager packages (steamos-manager & inputplumber)..."
|
||||
sudo pacman -S --needed --noconfirm steamos-manager inputplumber || { err "Installing hardware manager packages failed."; return 1; }
|
||||
sudo 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
|
||||
# the logout screen, and the machine never suspends on its own (e.g.
|
||||
# during a long download). The launcher's Shut Down still shuts down.
|
||||
kset machine powerdevilrc "AC|SuspendAndShutdown" PowerButtonAction 1
|
||||
kset machine powerdevilrc "AC|SuspendAndShutdown" AutoSuspendAction 0
|
||||
kset machine powerdevilrc "AC|Display" LockBeforeTurnOffDisplay false
|
||||
reload_powerdevil
|
||||
ok "Steam Machine support on."
|
||||
}
|
||||
|
||||
@@ -132,10 +422,17 @@ machine_disable() {
|
||||
info "Removing Steam Machine support..."
|
||||
systemctl --user disable --now steamos-manager.service 2>/dev/null
|
||||
sudo systemctl disable --now steamos-manager.service 2>/dev/null
|
||||
sudo pacman -Rns --noconfirm steamos-manager 2>/dev/null
|
||||
sudo systemctl disable --now inputplumber.service 2>/dev/null
|
||||
sudo pacman -Rns --noconfirm steamos-manager inputplumber 2>/dev/null
|
||||
sudo rm -f "$LED_UDEV_RULE" /etc/modules-load.d/leds-valve.conf
|
||||
sudo udevadm control --reload
|
||||
sudo modprobe -r leds-valve 2>/dev/null
|
||||
sudo pacman -Rns --noconfirm leds-valve-dkms-git 2>/dev/null
|
||||
krevert machine
|
||||
reload_powerdevil
|
||||
sudo systemctl disable ensure-kernel-headers.service 2>/dev/null
|
||||
sudo rm -f "$LED_DKMS_OVERRIDE" "$HEADERS_UNIT" "$HEADERS_SCRIPT"
|
||||
sudo rmdir "$(dirname "$HEADERS_SCRIPT")" 2>/dev/null
|
||||
sudo systemctl daemon-reload
|
||||
ok "Steam Machine support removed (the AUR helper, if installed, is kept)."
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
# SteamOS desktop extras that cachyos-vapor doesn't ship, taken from the
|
||||
# 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. 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 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
|
||||
EXTRAS_FILES=(
|
||||
/usr/local/bin/holo-add-to-steam
|
||||
/usr/local/bin/holo-nested-desktop
|
||||
/usr/local/share/kio/servicemenus/steam.desktop
|
||||
/usr/local/share/plasma/kickeractions/steam.desktop
|
||||
/usr/local/share/icons/hicolor/scalable/actions/gaming-return.svg
|
||||
"$NESTED_DIR"
|
||||
)
|
||||
# Group name for our rule in kwinrulesrc; KWin accepts any unique id.
|
||||
STEAM_KEYBOARD_RULE=cachyos-gamescope-boot-steam-keyboard
|
||||
|
||||
fetch_valve_presets() {
|
||||
# fetch_valve_presets <dir>: download and extract the newest
|
||||
# steamdeck-kde-presets into <dir>. The newest SteamOS release repo
|
||||
# (jupiter-3.9, jupiter-3.10, ...) is found on the mirror; its pacman
|
||||
# database gives the exact file name and SHA-256, which is verified.
|
||||
local dir="$1" repo desc file_name sha256
|
||||
repo="$(curl -fsL "$VALVE_MIRROR/" | grep -oE 'jupiter-[0-9]+\.[0-9]+/' | tr -d / | sort -V | tail -n 1)"
|
||||
[[ -n "$repo" ]] && curl -fsL "$VALVE_MIRROR/$repo/os/x86_64/$repo.db" -o "$dir/repo.db" ||
|
||||
{ err "Couldn't read Valve's SteamOS package index ($VALVE_MIRROR)."; return 1; }
|
||||
desc="$(tar -tf "$dir/repo.db" 2>/dev/null | grep -E '^steamdeck-kde-presets-[0-9][^/]*/desc$' | head -n 1)"
|
||||
[[ -n "$desc" ]] || { err "No steamdeck-kde-presets in Valve's $repo repository."; return 1; }
|
||||
file_name="$(tar -xOf "$dir/repo.db" "$desc" | awk '/^%FILENAME%$/ { getline; print }')"
|
||||
sha256="$(tar -xOf "$dir/repo.db" "$desc" | awk '/^%SHA256SUM%$/ { getline; print }')"
|
||||
|
||||
info "Downloading $file_name ($repo)..."
|
||||
curl -fsL "$VALVE_MIRROR/$repo/os/x86_64/$file_name" -o "$dir/presets.pkg.tar.zst" ||
|
||||
{ err "Downloading $file_name failed."; return 1; }
|
||||
echo "$sha256 $dir/presets.pkg.tar.zst" | sha256sum -c --quiet - ||
|
||||
{ err "Checksum mismatch for $file_name; not using it."; return 1; }
|
||||
tar -I unzstd -xf "$dir/presets.pkg.tar.zst" -C "$dir" usr/bin usr/share ||
|
||||
{ err "Extracting $file_name failed."; return 1; }
|
||||
}
|
||||
|
||||
extras_enable() {
|
||||
info "Installing SteamOS desktop extras (Add to Steam, Nested Desktop, icons)..."
|
||||
local pkg
|
||||
for pkg in curl zstd kdialog; do
|
||||
# holo-add-to-steam and holo-nested-desktop show errors with kdialog.
|
||||
pacman -Q "$pkg" >/dev/null 2>&1 && continue
|
||||
sudo pacman -S --needed --noconfirm "$pkg" && state_set theme "installed_$pkg" 1
|
||||
done
|
||||
|
||||
local tmp_dir src
|
||||
tmp_dir="$(mktemp -d)"
|
||||
if ! fetch_valve_presets "$tmp_dir"; then
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
src="$tmp_dir/usr"
|
||||
sudo install -D -m 755 "$src/bin/holo-add-to-steam" /usr/local/bin/holo-add-to-steam
|
||||
sudo install -D -m 755 "$src/bin/holo-nested-desktop" /usr/local/bin/holo-nested-desktop
|
||||
sudo install -D -m 644 "$src/share/kio/servicemenus/steam.desktop" /usr/local/share/kio/servicemenus/steam.desktop
|
||||
sudo install -D -m 644 "$src/share/plasma/kickeractions/steam.desktop" /usr/local/share/plasma/kickeractions/steam.desktop
|
||||
# gaming-return.svg is a symlink in the package; SteamOS uses it for the
|
||||
# Return to Gaming Mode shortcut on non-Deck hardware.
|
||||
sudo install -D -m 644 "$src/share/icons/hicolor/scalable/actions/steam-gaming-return.svg" \
|
||||
/usr/local/share/icons/hicolor/scalable/actions/gaming-return.svg
|
||||
# Nested Desktop with its Steam library art; the entry points at the
|
||||
# art under /usr/share, which is where Valve puts it.
|
||||
sudo rm -rf "$NESTED_DIR"
|
||||
sudo mkdir -p "$(dirname "$NESTED_DIR")"
|
||||
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"
|
||||
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
sudo gtk-update-icon-cache -q -f -t /usr/local/share/icons/hicolor 2>/dev/null || true
|
||||
kbuildsycoca6 >/dev/null 2>&1 || true
|
||||
set_shortcut_icon gaming-return
|
||||
|
||||
# Keep Steam's on-screen keyboard (Steam + X) above other windows and out
|
||||
# of the taskbar, like SteamOS's kwinrulesrc.
|
||||
local rules rule="$STEAM_KEYBOARD_RULE"
|
||||
rules="$(kreadconfig6 --file kwinrulesrc --group General --key rules)"
|
||||
if [[ ",$rules," != *",$rule,"* ]]; then
|
||||
kset theme kwinrulesrc "$rule" Description "Window settings for Steam Keyboard"
|
||||
kset theme kwinrulesrc "$rule" above true
|
||||
kset theme kwinrulesrc "$rule" aboverule 2
|
||||
kset theme kwinrulesrc "$rule" skiptaskbar true
|
||||
kset theme kwinrulesrc "$rule" skiptaskbarrule 2
|
||||
kset theme kwinrulesrc "$rule" title "Steam Keyboard"
|
||||
kset theme kwinrulesrc "$rule" titlematch 2
|
||||
kset theme kwinrulesrc "$rule" type 16
|
||||
kset theme kwinrulesrc "$rule" typerule 2
|
||||
kset theme kwinrulesrc "$rule" wmclass steam
|
||||
kset theme kwinrulesrc "$rule" wmclasscomplete true
|
||||
kset theme kwinrulesrc "$rule" wmclassmatch 2
|
||||
kset theme kwinrulesrc General rules "${rules:+$rules,}$rule"
|
||||
kset theme kwinrulesrc General count "$(( $(kreadconfig6 --file kwinrulesrc --group General --key count --default 0) + 1 ))"
|
||||
qdbus6 org.kde.KWin /KWin org.kde.KWin.reconfigure >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
extras_disable() {
|
||||
sudo rm -rf "${EXTRAS_FILES[@]}"
|
||||
sudo gtk-update-icon-cache -q -f -t /usr/local/share/icons/hicolor 2>/dev/null || true
|
||||
kbuildsycoca6 >/dev/null 2>&1 || true
|
||||
set_shortcut_icon steam
|
||||
local pkg
|
||||
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 is reverted by the theme's journal.
|
||||
return 0
|
||||
}
|
||||
+112
-246
@@ -1,269 +1,135 @@
|
||||
#!/bin/bash
|
||||
# Valve's SteamOS Vapor desktop theme and SteamOS desktop defaults.
|
||||
# Sourced by setup-gamescope-boot.sh; not meant to be run on its own.
|
||||
# SteamOS Vapor desktop theme, from CachyOS's cachyos-vapor package.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
merge_kde_config() {
|
||||
# Merge a KDE-style ini file (e.g. one of Valve's /etc/xdg defaults) into
|
||||
# the user's own config (journaled, see lib/state.sh), so SteamOS defaults win over
|
||||
# the distro's without touching package-owned files in /etc/xdg.
|
||||
# Nested groups ("[A][B]") are supported; immutable "[$i]" groups and
|
||||
# comments are skipped.
|
||||
local src="$1" target="$2" line key value
|
||||
local groups=""
|
||||
local skip=0
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
line="${line%$'\r'}"
|
||||
[[ -z "$line" || "$line" == \#* ]] && continue
|
||||
if [[ "$line" == \[* ]]; then
|
||||
skip=0; groups=""
|
||||
[[ "$line" == *'[$'* ]] && { skip=1; continue; }
|
||||
local rest="$line"
|
||||
while [[ "$rest" =~ ^\[([^]]*)\](.*)$ ]]; do
|
||||
groups+="${groups:+|}${BASH_REMATCH[1]}"
|
||||
rest="${BASH_REMATCH[2]}"
|
||||
done
|
||||
continue
|
||||
fi
|
||||
(( skip )) && continue
|
||||
[[ "$line" == *=* ]] || continue
|
||||
key="${line%%=*}"; value="${line#*=}"
|
||||
kset theme "$target" "$groups" "$key" "$value"
|
||||
done < "$src"
|
||||
}
|
||||
VAPOR_LOOKANDFEEL=com.valve.vapor.desktop
|
||||
VAPOR_DEFAULTS=/usr/share/plasma/look-and-feel/$VAPOR_LOOKANDFEEL/contents/defaults
|
||||
# Files the "Desktop and window layout" part replaces wholesale (panels,
|
||||
# widgets, wallpaper); backed up as-is rather than journaled key by key.
|
||||
THEME_LAYOUT_FILES=(plasma-org.kde.plasma.desktop-appletsrc plasmashellrc kdedefaults)
|
||||
|
||||
install_vapor_theme() {
|
||||
# Installs Valve's official SteamOS "Vapor" Plasma theme (color scheme,
|
||||
# Plasma look-and-feel package, wallpapers, icons) straight from Valve's
|
||||
# own SteamOS package mirror, into the current user's ~/.local/share.
|
||||
# This only touches the invoking user's home directory, never system
|
||||
# files, so it's independent from everything else this script does.
|
||||
local mirror="https://steamdeck-packages.steamos.cloud/archlinux-mirror"
|
||||
local repo file_name sha256 tmp_dir
|
||||
tmp_dir="$(mktemp -d)"
|
||||
|
||||
# Take the package from the newest SteamOS release repo (jupiter-3.9,
|
||||
# jupiter-3.10, ...), read from its pacman database: that gives the
|
||||
# exact file name and checksum. Fall back to a known-good version.
|
||||
info "Looking up the current SteamOS Vapor presets..."
|
||||
repo="$(curl -fsL "$mirror/" | grep -oE 'jupiter-[0-9]+\.[0-9]+/' | tr -d / | sort -V | tail -n 1)"
|
||||
if [[ -n "$repo" ]] && curl -fsL "$mirror/$repo/os/x86_64/$repo.db" -o "$tmp_dir/repo.db"; then
|
||||
local desc
|
||||
desc="$(tar -tf "$tmp_dir/repo.db" 2>/dev/null | grep -E '^steamdeck-kde-presets-[0-9][^/]*/desc$' | head -n 1)"
|
||||
if [[ -n "$desc" ]]; then
|
||||
file_name="$(tar -xOf "$tmp_dir/repo.db" "$desc" | awk '/^%FILENAME%$/ { getline; print }')"
|
||||
sha256="$(tar -xOf "$tmp_dir/repo.db" "$desc" | awk '/^%SHA256SUM%$/ { getline; print }')"
|
||||
fi
|
||||
fi
|
||||
if [[ -z "${file_name:-}" ]]; then
|
||||
warn "Couldn't read Valve's package index; using the known-good version."
|
||||
repo="jupiter-3.9"
|
||||
file_name="steamdeck-kde-presets-3.9.4-2-any.pkg.tar.zst"
|
||||
sha256=""
|
||||
fi
|
||||
|
||||
info "Downloading $file_name ($repo)..."
|
||||
if ! curl -fL "$mirror/$repo/os/x86_64/$file_name" -o "${tmp_dir}/presets.tar.zst"; then
|
||||
err "Download failed. Valve may have moved/renamed this package;"
|
||||
err "check $mirror for the current version."
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
if [[ -n "$sha256" ]] && ! echo "$sha256 ${tmp_dir}/presets.tar.zst" | sha256sum -c --quiet -; then
|
||||
err "Checksum mismatch for $file_name; not installing it."
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "Extracting package contents..."
|
||||
if ! tar -I unzstd -xf "${tmp_dir}/presets.tar.zst" -C "$tmp_dir"; then
|
||||
err "Extraction failed (is 'zstd' installed? try: sudo pacman -S --needed zstd)"
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
info "Creating local configuration directories..."
|
||||
mkdir -p ~/.local/share/color-schemes \
|
||||
~/.local/share/plasma/desktoptheme \
|
||||
~/.local/share/plasma/look-and-feel \
|
||||
~/.local/share/wallpapers \
|
||||
~/.local/share/icons/hicolor
|
||||
|
||||
info "Installing Steam Deck theme assets..."
|
||||
[[ -d "${tmp_dir}/usr/share/color-schemes" ]] && cp -r "${tmp_dir}/usr/share/color-schemes/"* ~/.local/share/color-schemes/
|
||||
[[ -d "${tmp_dir}/usr/share/plasma/desktoptheme/Vapor" ]] && cp -r "${tmp_dir}/usr/share/plasma/desktoptheme/Vapor" ~/.local/share/plasma/desktoptheme/
|
||||
[[ -d "${tmp_dir}/usr/share/plasma/look-and-feel/com.valve.vapor.desktop" ]] && cp -r "${tmp_dir}/usr/share/plasma/look-and-feel/com.valve.vapor.desktop" ~/.local/share/plasma/look-and-feel/
|
||||
[[ -d "${tmp_dir}/usr/share/wallpapers" ]] && cp -r "${tmp_dir}/usr/share/wallpapers/"* ~/.local/share/wallpapers/
|
||||
[[ -d "${tmp_dir}/usr/share/icons/hicolor" ]] && cp -r "${tmp_dir}/usr/share/icons/hicolor/"* ~/.local/share/icons/hicolor/
|
||||
|
||||
# The rest of what SteamOS ships: GTK theme (Steam, Firefox, ...), Konsole
|
||||
# profile and user avatars -- the same files the AUR package installs.
|
||||
mkdir -p ~/.local/share/themes ~/.local/share/konsole ~/.local/share/plasma/avatars
|
||||
[[ -d "${tmp_dir}/usr/share/themes/Vapor" ]] && cp -r "${tmp_dir}/usr/share/themes/Vapor" ~/.local/share/themes/
|
||||
[[ -d "${tmp_dir}/usr/share/konsole" ]] && cp -r "${tmp_dir}/usr/share/konsole/"* ~/.local/share/konsole/
|
||||
[[ -d "${tmp_dir}/usr/share/plasma/avatars" ]] && cp -r "${tmp_dir}/usr/share/plasma/avatars/"* ~/.local/share/plasma/avatars/
|
||||
|
||||
# Valve's SteamOS desktop defaults (fonts, Steam keyboard window rule,
|
||||
# no welcome screen, light file indexing, GTK theme).
|
||||
# Left out on purpose: autostart entries (Steam is started by our user
|
||||
# unit), sddm/profile.d, and files hardcoding /home/deck or SteamOS tools.
|
||||
info "Applying SteamOS desktop defaults..."
|
||||
local f
|
||||
# kscreenlockerrc is left to the single-user question.
|
||||
for f in kdeglobals kwinrc kwinrulesrc kded5rc baloofilerc kcminputrc; do
|
||||
[[ -f "${tmp_dir}/etc/xdg/$f" ]] && merge_kde_config "${tmp_dir}/etc/xdg/$f" "$f"
|
||||
done
|
||||
[[ -f "${tmp_dir}/etc/xdg/kded5rc" ]] && merge_kde_config "${tmp_dir}/etc/xdg/kded5rc" kded6rc
|
||||
kset theme konsolerc "Desktop Entry" DefaultProfile Vapor.profile
|
||||
mkdir -p ~/.config/gtk-3.0 ~/.config/gtk-4.0
|
||||
kset theme ~/.config/gtk-3.0/settings.ini Settings gtk-theme-name Vapor
|
||||
kset theme ~/.config/gtk-4.0/settings.ini Settings gtk-theme-name Vapor
|
||||
# Vapor is a dark theme: tell GTK, libadwaita and portal-aware apps
|
||||
# (Firefox, Steam's web views, Flatpaks) to use their dark variants too.
|
||||
kset theme ~/.config/gtk-3.0/settings.ini Settings gtk-application-prefer-dark-theme true
|
||||
kset theme ~/.config/gtk-4.0/settings.ini Settings gtk-application-prefer-dark-theme true
|
||||
if command -v gsettings >/dev/null 2>&1; then
|
||||
[[ -n "$(state_get theme gsettings_scheme)" ]] ||
|
||||
state_set theme gsettings_scheme "$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null)"
|
||||
[[ -n "$(state_get theme gsettings_gtk)" ]] ||
|
||||
state_set theme gsettings_gtk "$(gsettings get org.gnome.desktop.interface gtk-theme 2>/dev/null)"
|
||||
gsettings set org.gnome.desktop.interface color-scheme prefer-dark 2>/dev/null || true
|
||||
gsettings set org.gnome.desktop.interface gtk-theme Vapor 2>/dev/null || true
|
||||
fi
|
||||
[[ -f "${tmp_dir}/etc/xdg/gtk-2.0/gtkrc" ]] && cp "${tmp_dir}/etc/xdg/gtk-2.0/gtkrc" ~/.gtkrc-2.0
|
||||
|
||||
info "Cleaning up temporary files..."
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
# Remember what to go back to when the theme is turned off again.
|
||||
[[ -n "$(state_get theme lookandfeel)" ]] ||
|
||||
state_set theme lookandfeel "$(kreadconfig6 --file kdeglobals --group KDE --key LookAndFeelPackage \
|
||||
--default "$(kreadconfig6 --file /etc/xdg/kdeglobals --group KDE --key LookAndFeelPackage --default org.kde.breeze.desktop)")"
|
||||
[[ -n "$(state_get theme colorscheme)" ]] ||
|
||||
state_set theme colorscheme "$(kreadconfig6 --file kdeglobals --group General --key ColorScheme --default BreezeLight)"
|
||||
|
||||
local appletsrc="plasma-org.kde.plasma.desktop-appletsrc" applet
|
||||
|
||||
# Same launcher icon SteamOS sets (Vapor Deck plasmoid setup script).
|
||||
for applet in $(plasma_applets org.kde.plasma.kickoff); do
|
||||
kset theme "$appletsrc" "Containments|${applet%%:*}|Applets|${applet#*:}|Configuration|General" icon distributor-logo-steamdeck
|
||||
done
|
||||
# SteamOS scales tray icons to the panel height. Valve's setup script
|
||||
# targets Plasma 5's separate tray containment; in Plasma 6 the setting
|
||||
# lives on the systemtray applet itself.
|
||||
for applet in $(plasma_applets org.kde.plasma.systemtray); do
|
||||
kset theme "$appletsrc" "Containments|${applet%%:*}|Applets|${applet#*:}|General" scaleIconsToFit true
|
||||
done
|
||||
# SteamOS shows the date below the time.
|
||||
for applet in $(plasma_applets org.kde.plasma.digitalclock); do
|
||||
kset theme "$appletsrc" "Containments|${applet%%:*}|Applets|${applet#*:}|Configuration|Appearance" dateDisplayFormat BelowTime
|
||||
done
|
||||
|
||||
# Wallpaper: Valve ships them as flat JPGs in usr/share/wallpapers.
|
||||
local wallpaper="$HOME/.local/share/wallpapers/Steam Deck Logo Default.jpg" containment
|
||||
if [[ -f "$wallpaper" ]]; then
|
||||
for containment in $(grep -oP '^\[Containments\]\[\K[0-9]+(?=\]$)' ~/.config/"$appletsrc" 2>/dev/null); do
|
||||
grep -q "^\[Containments\]\[$containment\]\[Wallpaper\]" ~/.config/"$appletsrc" || continue
|
||||
kset theme "$appletsrc" "Containments|$containment|Wallpaper|org.kde.image|General" Image "file://$wallpaper"
|
||||
current_colorscheme() {
|
||||
# CachyOS ships its dark Breeze colors inline in kdeglobals without a
|
||||
# ColorScheme name, so fall back to the scheme matching ColorSchemeHash.
|
||||
local name hash f
|
||||
name="$(kreadconfig6 --file kdeglobals --group General --key ColorScheme)"
|
||||
if [[ -z "$name" ]]; then
|
||||
hash="$(kreadconfig6 --file kdeglobals --group General --key ColorSchemeHash)"
|
||||
for f in ~/.local/share/color-schemes/*.colors /usr/share/color-schemes/*.colors; do
|
||||
[[ -f "$f" && -n "$hash" ]] || continue
|
||||
[[ "$(sha1sum < "$f")" == "$hash "* ]] && { name="$(basename "$f" .colors)"; break; }
|
||||
done
|
||||
fi
|
||||
echo "${name:-BreezeLight}"
|
||||
}
|
||||
|
||||
# Vapor color scheme and Plasma style; the theme's own defaults use
|
||||
# breeze-dark icons (there is no "Vapor" icon theme).
|
||||
kset theme kdeglobals KDE LookAndFeelPackage com.valve.vapor.desktop
|
||||
kset theme kdeglobals Icons Theme breeze-dark
|
||||
kset theme kdeglobals General ColorScheme Vapor
|
||||
kset theme plasmarc Theme name Vapor
|
||||
|
||||
# SteamOS uses a solid, full-width bottom panel at Plasma's stock 44px
|
||||
# height (CachyOS ships a floating 30px one).
|
||||
local panel
|
||||
for panel in $(grep -oP '^\[PlasmaViews\]\[Panel \K[0-9]+(?=\]$)' ~/.config/plasmashellrc 2>/dev/null); do
|
||||
kset theme plasmashellrc "PlasmaViews|Panel $panel" floating 0
|
||||
kset theme plasmashellrc "PlasmaViews|Panel $panel|Defaults" thickness 44
|
||||
done
|
||||
|
||||
gtk-update-icon-cache -f -t ~/.local/share/icons/hicolor 2>/dev/null || true
|
||||
kbuildsycoca6 --noincremental 2>/dev/null || true
|
||||
|
||||
# Live changes need a running Plasma session; otherwise they are picked
|
||||
# up from the config files above at the next Plasma login.
|
||||
if [[ "${PLASMASHELL_WAS_RUNNING:-false}" == true ]]; then
|
||||
info "Applying the theme to the running Plasma session..."
|
||||
lookandfeeltool -a com.valve.vapor.desktop >/dev/null 2>&1 || true
|
||||
# lookandfeeltool leaves the distro's color scheme active; apply it explicitly.
|
||||
plasma-apply-colorscheme Vapor >/dev/null 2>&1 || true
|
||||
qdbus6 org.kde.kded6 /modules/gtkconfig org.kde.gtkconfig.setGtkTheme Vapor >/dev/null 2>&1 || true
|
||||
qdbus6 org.kde.KWin /KWin org.kde.KWin.reconfigure >/dev/null 2>&1 || true
|
||||
else
|
||||
info "No running Plasma session; the theme applies at your next desktop login."
|
||||
fi
|
||||
ok "SteamOS desktop look installed."
|
||||
journal_vapor_defaults() {
|
||||
# Write every setting from Vapor's defaults file ("[file][group]" +
|
||||
# key=value) through the undo journal, so disable restores the user's
|
||||
# icons, cursor, splash, window decoration etc. The color scheme is
|
||||
# left to plasma-apply-colorscheme.
|
||||
local line file groups key rest
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" =~ ^\[([^]]+)\](.*)$ ]]; then
|
||||
file="${BASH_REMATCH[1]}"; rest="${BASH_REMATCH[2]}"; groups=""
|
||||
while [[ "$rest" =~ ^\[([^]]*)\](.*)$ ]]; do
|
||||
groups+="${groups:+|}${BASH_REMATCH[1]}"; rest="${BASH_REMATCH[2]}"
|
||||
done
|
||||
elif [[ "$line" == *=* && -n "$groups" ]]; then
|
||||
key="${line%%=*}"
|
||||
[[ "$key" == ColorScheme ]] && continue
|
||||
kset theme "$file" "$groups" "$key" "${line#*=}"
|
||||
fi
|
||||
done < "$VAPOR_DEFAULTS"
|
||||
}
|
||||
|
||||
theme_status() {
|
||||
[[ "$(kreadconfig6 --file kdeglobals --group KDE --key LookAndFeelPackage)" == com.valve.vapor.desktop ]]
|
||||
[[ "$(kreadconfig6 --file kdeglobals --group KDE --key LookAndFeelPackage)" == "$VAPOR_LOOKANDFEEL" ]]
|
||||
}
|
||||
|
||||
theme_enable() {
|
||||
command -v curl >/dev/null 2>&1 || sudo pacman -S --needed --noconfirm curl
|
||||
command -v unzstd >/dev/null 2>&1 || sudo pacman -S --needed --noconfirm zstd
|
||||
# Stop plasmashell first: the panel and wallpaper edits go to files it
|
||||
# would otherwise overwrite with its in-memory config on exit.
|
||||
stop_plasmashell_for_edit
|
||||
install_vapor_theme
|
||||
local rc=$?
|
||||
restart_plasmashell_if_stopped
|
||||
(( rc == 0 )) || warn "Vapor theme install ran into a problem - see errors above."
|
||||
return $rc
|
||||
if ! pacman -Q cachyos-vapor >/dev/null 2>&1; then
|
||||
info "Installing cachyos-vapor..."
|
||||
sudo pacman -S --needed --noconfirm cachyos-vapor || { err "Installing cachyos-vapor failed."; return 1; }
|
||||
# Only remove it again on disable if we were the ones installing it.
|
||||
state_set theme installed_pkg 1
|
||||
fi
|
||||
if [[ -z "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]]; then
|
||||
err "Run this from the Plasma desktop: applying the Vapor layout needs a running Plasma session."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Only the first time, so a re-apply doesn't overwrite the originals.
|
||||
local backup="$STATE_DIR/theme-layout" f
|
||||
if [[ ! -d "$backup" ]]; then
|
||||
mkdir -p "$backup"
|
||||
for f in "${THEME_LAYOUT_FILES[@]}"; do
|
||||
[[ -e ~/.config/"$f" ]] && cp -a ~/.config/"$f" "$backup/"
|
||||
done
|
||||
fi
|
||||
# Applying a color scheme or cursor rewrites more than the journal
|
||||
# covers (inline colors, GTK settings): remember what to re-apply.
|
||||
[[ -n "$(state_get theme colorscheme)" ]] || state_set theme colorscheme "$(current_colorscheme)"
|
||||
[[ -n "$(state_get theme cursor)" ]] ||
|
||||
state_set theme cursor "$(kreadconfig6 --file kcminputrc --group Mouse --key cursorTheme --default breeze_cursors)"
|
||||
for f in ~/.config/gtk-3.0/settings.ini ~/.config/gtk-4.0/settings.ini; do
|
||||
[[ -f "$f" ]] && kset theme "$f" Settings gtk-cursor-theme-name \
|
||||
"$(kreadconfig6 --file "$f" --group Settings --key gtk-cursor-theme-name)"
|
||||
done
|
||||
|
||||
kset theme kdeglobals KDE LookAndFeelPackage "$VAPOR_LOOKANDFEEL"
|
||||
# Vapor is a dark theme: register it as the dark one, or Plasma's Dark
|
||||
# Mode toggle shows "off" and offers to switch to Breeze Dark.
|
||||
kset theme kdeglobals KDE DefaultDarkLookAndFeel "$VAPOR_LOOKANDFEEL"
|
||||
journal_vapor_defaults
|
||||
# Journal the key, then clear it: plasma-apply-colorscheme skips a
|
||||
# scheme that is already named in the config, without writing its colors.
|
||||
kset theme kdeglobals General ColorScheme --delete
|
||||
plasma-apply-colorscheme Vapor >/dev/null 2>&1 || true
|
||||
|
||||
info "Applying the Vapor theme with its desktop and window layout..."
|
||||
# --resetLayout is "Desktop and window layout" in System Settings: the
|
||||
# SteamOS panel, launcher icon and wallpaper. It needs plasmashell up.
|
||||
lookandfeeltool -a "$VAPOR_LOOKANDFEEL" --resetLayout >/dev/null 2>&1 ||
|
||||
{ err "lookandfeeltool failed."; return 1; }
|
||||
# The new layout has a new launcher, without single user's settings.
|
||||
if single_status; then
|
||||
sleep 3
|
||||
stop_plasmashell_for_edit
|
||||
single_launcher
|
||||
restart_plasmashell_if_stopped
|
||||
fi
|
||||
extras_enable || warn "SteamOS desktop extras not installed (see above); the theme itself is on."
|
||||
ok "Vapor theme active."
|
||||
}
|
||||
|
||||
theme_disable() {
|
||||
info "Restoring the previous desktop look..."
|
||||
local lookandfeel colorscheme
|
||||
lookandfeel="$(state_get theme lookandfeel org.kde.breeze.desktop)"
|
||||
colorscheme="$(state_get theme colorscheme BreezeLight)"
|
||||
extras_disable
|
||||
info "Restoring the previous desktop look and layout..."
|
||||
plasma-apply-colorscheme "$(state_get theme colorscheme BreezeLight)" >/dev/null 2>&1 || true
|
||||
plasma-apply-cursortheme "$(state_get theme cursor breeze_cursors)" >/dev/null 2>&1 || true
|
||||
|
||||
local backup="$STATE_DIR/theme-layout" f
|
||||
stop_plasmashell_for_edit
|
||||
if has_journal theme; then
|
||||
krevert theme
|
||||
else
|
||||
# Installed by a version of this script without the undo journal:
|
||||
# undo the main settings it made.
|
||||
kwriteconfig6 --file kdeglobals --group KDE --key LookAndFeelPackage --delete
|
||||
kwriteconfig6 --file kdeglobals --group General --key ColorScheme --delete
|
||||
kwriteconfig6 --file kdeglobals --group Icons --key Theme --delete
|
||||
kwriteconfig6 --file plasmarc --group Theme --key name --delete
|
||||
local f
|
||||
for f in ~/.config/gtk-3.0/settings.ini ~/.config/gtk-4.0/settings.ini; do
|
||||
[[ -f "$f" ]] || continue
|
||||
kwriteconfig6 --file "$f" --group Settings --key gtk-theme-name --delete
|
||||
kwriteconfig6 --file "$f" --group Settings --key gtk-application-prefer-dark-theme --delete
|
||||
if [[ -d "$backup" ]]; then
|
||||
for f in "${THEME_LAYOUT_FILES[@]}"; do
|
||||
rm -rf ~/.config/"$f"
|
||||
[[ -e "$backup/$f" ]] && cp -a "$backup/$f" ~/.config/
|
||||
done
|
||||
kwriteconfig6 --file konsolerc --group "Desktop Entry" --key DefaultProfile --delete
|
||||
fi
|
||||
if command -v gsettings >/dev/null 2>&1; then
|
||||
local v
|
||||
v="$(state_get theme gsettings_scheme)"
|
||||
if [[ -n "$v" ]]; then gsettings set org.gnome.desktop.interface color-scheme "$v" 2>/dev/null
|
||||
else gsettings reset org.gnome.desktop.interface color-scheme 2>/dev/null; fi
|
||||
v="$(state_get theme gsettings_gtk)"
|
||||
if [[ -n "$v" ]]; then gsettings set org.gnome.desktop.interface gtk-theme "$v" 2>/dev/null
|
||||
else gsettings reset org.gnome.desktop.interface gtk-theme 2>/dev/null; fi
|
||||
fi
|
||||
rm -rf ~/.local/share/plasma/look-and-feel/com.valve.vapor.desktop \
|
||||
~/.local/share/plasma/desktoptheme/Vapor \
|
||||
~/.local/share/themes/Vapor \
|
||||
~/.local/share/color-schemes/Vapor.colors ~/.local/share/color-schemes/VGUI.colors \
|
||||
~/.local/share/konsole/Vapor.profile ~/.local/share/konsole/Vapor.colorscheme \
|
||||
~/.local/share/wallpapers/Steam\ Deck\ Logo*.jpg ~/.local/share/wallpapers/Troll.jpg
|
||||
rm -f ~/.gtkrc-2.0
|
||||
kbuildsycoca6 --noincremental 2>/dev/null || true
|
||||
|
||||
if [[ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]]; then
|
||||
lookandfeeltool -a "$lookandfeel" >/dev/null 2>&1 || true
|
||||
plasma-apply-colorscheme "$colorscheme" >/dev/null 2>&1 || true
|
||||
rm -rf "$backup"
|
||||
fi
|
||||
# After the tools above, so keys that weren't set before are removed again.
|
||||
krevert theme
|
||||
single_status && single_launcher
|
||||
restart_plasmashell_if_stopped
|
||||
qdbus6 org.kde.KWin /KWin org.kde.KWin.reconfigure >/dev/null 2>&1 || true
|
||||
|
||||
if [[ -n "$(state_get theme installed_pkg)" ]]; then
|
||||
# Fails, and stays installed, when another package (e.g.
|
||||
# cachyos-handheld) depends on it.
|
||||
sudo pacman -R --noconfirm cachyos-vapor >/dev/null 2>&1 ||
|
||||
info "Keeping cachyos-vapor installed (another package needs it)."
|
||||
fi
|
||||
state_clear theme
|
||||
ok "Previous desktop look restored."
|
||||
}
|
||||
@@ -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,82 +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
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
for lib in common state packages login-manager single-user steam-desktop steam-machine vapor-theme 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}"
|
||||
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.3.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
|
||||
+1142
File diff suppressed because it is too large.
Load diff
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
+400
@@ -0,0 +1,400 @@
|
||||
#!/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, str)
|
||||
def apply(self, ids, boot, reapply, password, hdmi):
|
||||
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]
|
||||
if hdmi: # rates confirmed on the HDMI screen
|
||||
args += ["--hdmi", hdmi]
|
||||
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)
|
||||
|
||||
@Slot(str)
|
||||
def hdmiOptions(self, password):
|
||||
"""Reads the HDMI displays: ends with an hdmi-options event."""
|
||||
self._start(["hdmi-options"], password)
|
||||
|
||||
@Slot(str, str, int, int, float, str)
|
||||
def hdmiTry(self, password, conn, w, h, hz, rates):
|
||||
"""Switches to hz live: an hdmi-tried event says whether it worked."""
|
||||
self._start(["hdmi-try", conn, str(w), str(h), "%g" % hz] + [r for r in rates.split(",") if r], password)
|
||||
|
||||
@Slot(str, str, int, int, float)
|
||||
def hdmiReset(self, password, conn, w, h, hz):
|
||||
"""The display's own EDID and the old rate again (hdmi-reset event)."""
|
||||
self._start(["hdmi-reset", conn, str(w), str(h), "%g" % hz], password)
|
||||
|
||||
@Slot(str, str)
|
||||
def hdmiForget(self, password, display):
|
||||
"""Removes a saved display (hdmi-forgotten event)."""
|
||||
self._start(["hdmi-forget", display], 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