Compare commits

..
55 Commits
Author SHA1 Message Date
theupriser 7bef834b16 Merge pull request #26 from theupriser/feat/vram-booster
2.3.0: VRAM booster, New badge
2026-09-27 13:05:19 +02:00
theupriser 86b9744227 feat: VRAM booster greyed out with an NVIDIA card, with why 2026-09-27 13:02:13 +02:00
theupriser b0947d7a2f feat: VRAM booster only for GPUs with at least 2 GB of their own VRAM 2026-09-27 12:55:40 +02:00
theupriser 3f97c85c3c feat: VRAM booster for every supported GPU, not only the Steam Machine 2026-09-27 12:51:47 +02:00
theupriser fd162c1e2e feat: New badge for default sub-options added in this version 2026-09-27 12:49:49 +02:00
theupriser 54ebddfe98 feat: VRAM booster for the Steam Machine 2026-09-27 12:48:01 +02:00
theupriser fe93dd3e67 Merge pull request #25 from theupriser/feat/fremont-poweroff
Feat/fremont poweroff
2026-09-27 12:41:38 +02:00
theupriser ef585c0db1 docs: Changelog entries per commit for 2.2.0 2026-09-27 12:36:25 +02:00
theupriser 32795bd5a5 feat(ui): Update badge on the progress screen 2026-09-27 12:35:40 +02:00
theupriser 3c8ca65902 fix(ui): Never write the sudo password to a file; Update label on the progress screen
The askpass helper reads the password from a named pipe that the app
feeds from memory each time sudo asks, instead of from a file.
2026-09-27 12:33:47 +02:00
theupriser 11a91ef85c feat: Update badge on components that get a newer version 2026-09-27 12:31:29 +02:00
theupriser 163f7908c4 feat: Feature versions are Steamify versions; Steam Machine support is 2.2.0 2026-09-27 12:27:55 +02:00
theupriser e121b06fd8 fix: Remove whatever is left of HDMI refresh boost, pin or no pin 2026-09-27 12:27:08 +02:00
theupriser 19ca966913 docs: Drop the kernel pin and HDMI refresh boost descriptions 2026-09-27 12:25:08 +02:00
theupriser 56fb9080a7 docs: Power-off fix texts cover every affected kernel, not just 7.2 2026-09-27 12:23:54 +02:00
theupriser 6df19eba3e feat: Power-off fix as its own sub-option, drop the kernel pin, feature versions
The power-off fix becomes an opt-out sub-option of Steam Machine
support. The kernel pin is no longer offered: an existing pin (and HDMI
refresh boost, which needs it) is unticked so a normal run removes it.
Components record a feature version on each successful run; installs
with an older version are ticked and updated, and new default
sub-options are ticked where their parent is on.
2026-09-27 12:20:56 +02:00
theupriser 7694c97d68 feat: Power-off fix for the Steam Machine; the kernel pin is optional
Kernels >= 7.2 keep the firmware's S4/S5 wake bit on GPIO pin 18, so the
Steam Machine starts again right after powering off. Steam Machine
support now builds a DKMS module (patches/steamify-fremont-poweroff.c)
that clears it before power-off, for every kernel. The kernel pin is no
longer ticked by default. Module sources and patches move to patches/,
embedded by the single-file build.
2026-09-27 12:13:23 +02:00
theupriser a5e644d71a Merge pull request #24 from theupriser/feat/hdmi-per-display
Feat/hdmi per display
2026-09-26 16:49:56 +02:00
theupriser 929ecfb349 feat(ui): Manage button for HDMI refresh boost
The item's switch becomes a Set up / Manage button. Manage lists the
saved displays, removes one with its Remove button, and sets up the
connected display when it has no saved rates.
2026-09-26 16:48:21 +02:00
theupriser 4cc87aaea6 feat: HDMI refresh boost per display, with a Saved displays screen
Rates are saved per display (EDID ID) and loaded by the hotplug script
for whichever display is connected. The item is on only while the
connected display is boosted; unticking removes just that one. The app
lists the saved displays and removes any of them (hdmi-forget).
2026-09-26 16:13:45 +02:00
theupriser 36d0a9d780 feat: HDMI refresh boost only for the display it was set up for
The EDID override moves off the kernel command line to a hotplug script
(boot unit + udev rule) that loads it only while the display it was
tested on is connected, identified over DDC; other displays get their
own EDID.
2026-09-26 16:04:02 +02:00
theupriser cb6361b49b Merge pull request #23 from theupriser/fix/fremont-cec
fix: HDMI-CEC on the Steam Machine
2026-09-26 11:30:29 +02:00
theupriser c510f9e2e5 fix: HDMI-CEC on the Steam Machine 2026-09-26 11:29:34 +02:00
theupriser 05a7d50c3f Merge pull request #22 from theupriser/docs/disclaimer
docs: Non-affiliation disclaimer in the README
2026-09-26 10:54:06 +02:00
theupriser badfd846d8 docs: Non-affiliation disclaimer in the README 2026-09-26 10:53:42 +02:00
theupriser ff364ffd69 Merge pull request #21 from theupriser/feat/app-shortcut
feat: The Steamify shortcut opens the app
2026-09-25 21:33:14 +02:00
theupriser 2b3f1de75a feat: The Steamify shortcut opens the app
Steamify CachyOS on the desktop and in the launcher starts the newest app
(in Konsole the first time, while PySide6 still has to be installed); the
terminal menu is Steamify Terminal in the launcher. A shortcut from before
2.0.1 is ticked in the menu, so a normal run replaces it. steamify-app.sh
leaves the shortcut's launcher entry (same id) alone.
2026-09-25 21:32:24 +02:00
theupriser 6d7d5861d6 Merge pull request #20 from theupriser/feat/v2-ui
Feat/v2 UI
2026-09-25 21:24:11 +02:00
theupriser 444ce29e57 Merge remote-tracking branch 'origin/main' into feat/v2-ui
# Conflicts:
#	AGENTS.md
#	CHANGELOG.md
#	lib/hdmi-refresh.sh
#	steamify.sh
2026-09-25 21:21:31 +02:00
theupriser 23ef5ef7ab feat(ui): HDMI refresh boost in the app
Set up… opens a screen with the display, its resolution and the rates on
offer. The display's own faster mode is switched to right away; the other
rates can each be tried live (wait for the picture, keep within 15 s or it
switches back) and ticked; Install installs the ticked ones through
apply --hdmi. All rates are loaded when the screen opens, so a try is only a
mode switch. Also fixes switching back to a rate like 59.97 Hz.
2026-09-25 21:21:13 +02:00
theupriser 178ee3405e fix(ui): The details panel scrolls instead of overflowing
Long texts (e.g. HDMI refresh boost) ran out of the card. The details are
in a Flickable now, which starts at the top for each item.
2026-09-25 20:42:08 +02:00
theupriser 601c05329d chore: Version 2.0.0 for the v2 app 2026-09-25 20:39:57 +02:00
theupriser 8ae9673eb2 Merge pull request #19 from theupriser/feat/hdmi-refresh-boost
Feat/hdmi refresh boost
2026-09-25 20:39:13 +02:00
theupriser dbafc43f2e feat(ui): HDMI refresh boost in the app, off only
The app gets its texts and the same rules as the menu: unticking Steam
Machine support or the kernel pin unticks it. It can't be turned on from
the app, as each step needs someone at the keyboard to confirm the picture.
2026-09-25 20:39:00 +02:00
theupriser ae8ed85fee feat: HDMI refresh boost and Update BIOS under Steam Machine support
Both are sub-options of Steam Machine support now, shown only while it is
ticked. Unticking it unticks them; unticking the kernel pin unticks the
HDMI refresh boost, which only works with the pinned kernel.
2026-09-25 20:37:56 +02:00
theupriser 00bcaea712 feat: HDMI refresh boost and Update BIOS under Steam Machine support
Both are sub-options of Steam Machine support now, shown only while it is
ticked. Unticking it unticks them; unticking the kernel pin unticks the
HDMI refresh boost, which only works with the pinned kernel.
2026-09-25 20:37:47 +02:00
theupriser a898d23b13 feat: HDMI refresh boost (Steam Machine, pinned kernel)
The pinned 7.1.6 kernel doesn't read the extra EDID block (HDMI Forum
EEODB) where monitors list their fast modes, and does no HDMI 2.1, so HDMI
displays often stay at 60 Hz. The new menu item reads the display's EDID
over DDC, calculates the highest rate that fits HDMI 2.0 at the desktop
resolution (rounded down to ten, plus the hundred below as a safe option),
tests each step live with a confirmation, and makes the confirmed ones
permanent through drm.edid_firmware (Limine, sdboot-manage or GRUB).
2026-09-25 20:33:46 +02:00
theupriser 3e53257fe2 feat: HDMI refresh boost (Steam Machine, pinned kernel)
The pinned 7.1.6 kernel doesn't read the extra EDID block (HDMI Forum
EEODB) where monitors list their fast modes, and does no HDMI 2.1, so HDMI
displays often stay at 60 Hz. The new menu item reads the display's EDID
over DDC, calculates the highest rate that fits HDMI 2.0 at the desktop
resolution (rounded down to ten, plus the hundred below as a safe option),
tests each step live with a confirmation, and makes the confirmed ones
permanent through drm.edid_firmware (Limine, sdboot-manage or GRUB).
2026-09-25 20:32:46 +02:00
theupriser a1eab9ac79 feat: Preview builds of feat/v2-ui; the app stretches to the window
Pushes to feat/v2-ui publish the pre-release v2-ui-preview (never latest);
its steamify-app.sh downloads that build (STEAMIFY_RELEASE). The app's
1280x720 layout now stretches to the window's shape, so the bars reach the
edges; re-apply resets when back on the menu.
2026-09-25 16:04:59 +02:00
theupriser c9c16b9eec fix(ui): Review and progress lists scroll; progress follows the running step 2026-09-25 16:02:19 +02:00
theupriser b17addd60f fix: Quote the backend's done event for shellcheck 2026-09-25 16:00:12 +02:00
theupriser ac613f05b3 feat: GitHub builds the app; run it with curl
steamify-app.tar.gz (ui/, steamify.sh, lib/, icon) and its launcher
steamify-app.sh in every release: it installs to ~/.local/share/steamify/app,
installs PySide6 if needed, adds a launcher entry and starts the app.
STEAMIFY_BRANCH=<branch> runs a branch before it's released.
2026-09-25 15:59:47 +02:00
theupriser 3a0eefab11 fix(ui): Applying got stuck: the ticked items came in as a JS value
Convert QML's array; if starting fails anyway, show the done screen with
the reason instead of staying on the progress screen.
2026-09-25 15:56:41 +02:00
theupriser 96558a9ca8 feat(ui): BIOS check runs invisibly, then the warnings, then the progress bar 2026-09-25 15:55:15 +02:00
theupriser 7c48b3d806 feat: BIOS update in the app, with both warnings
bios_enable split into bios_prepare (download, checksum, fwupd check) and
bios_flash; the terminal flow is unchanged. The app: check, warning 1,
warning 2 (type UPDATE, or hold A/OK for 5 seconds), flash, restart.
2026-09-25 15:53:30 +02:00
theupriser 396459b511 fix(ui): Controls aligned on the right, sub-items fold instead of the list jumping 2026-09-25 15:48:38 +02:00
theupriser 43a9541104 feat: Steamify app (v2 UI), first working version
ui/steamify-ui (PySide6 + QML): menu, review, password, applying and done
screens from the design, scaled to the window. Controller via evdev,
keyboard, TV remote (cecd). steamify.sh --backend status|apply (lib/backend.sh)
drives the same components; sudo asks through an askpass helper.
2026-09-25 15:46:02 +02:00
theupriser e09fe1609b Merge pull request #18 from theupriser/fix/no-lock-before-restart
fix: No lock screen in single user mode before a restart
2026-09-25 14:10:52 +02:00
theupriser 9dcac5cf08 fix: No lock screen in single user mode before a restart
The screen locker only read kscreenlockerrc at the next login, so the PC
still locked after idling until then. Reload it over D-Bus.
2026-09-25 14:01:00 +02:00
theupriser f91dd1f0b1 Merge pull request #16 from theupriser/fix/cec-steam-ui
Fix/cec steam UI
2026-09-25 13:18:52 +02:00
theupriser 0411429654 chore: Version 1.1.3 2026-09-25 13:17:02 +02:00
theupriser 1aa5e955fe fix: Repair HDMI-CEC set up by 1.1.0/1.1.1
It only counts as on with the steam-launcher drop-in; installed without it,
the menu ticks it, so a normal run adds it.
2026-09-25 13:16:48 +02:00
theupriser ce75e895c4 fix: Show the HDMI-CEC settings in Steam
CachyOS's gaming mode script sets STEAM_ENABLE_CEC=0. A drop-in for
steam-launcher.service overrides it with a later EnvironmentFile=.
2026-09-25 13:16:48 +02:00
theupriser 9aef0a986a Merge pull request #15 from theupriser/fix/cec-steam-settings
fix: Steam's HDMI-CEC settings reach cecd on a first run
2026-09-25 12:19:54 +02:00
theupriser f7aa2412df fix: Steam's HDMI-CEC settings reach cecd on a first run
HDMI-CEC runs before Steam Machine support, which installs steamos-manager,
so its configure-cecd unit couldn't be enabled. Link them from Steam Machine
support too.
2026-09-25 11:13:53 +02:00
25 changed files with 3602 additions and 146 deletions

No files matched your search

+17
View File
@@ -31,6 +31,23 @@ version="v$(grep -oP '^VERSION=\K.*' "$entry"), $(git describe --always --dirty
sed -e '1{/^#!/d}' -e '/^# Sourced by steamify.sh/d' "lib/$lib.sh"
echo
done
# 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.
+43 -4
View File
@@ -5,12 +5,18 @@
# 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:
@@ -28,17 +34,26 @@ jobs:
- name: Build
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/steamify.sh
# SC2034/SC2154: variables used across the inlined modules.
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: steamify
path: dist/steamify.sh
path: |
dist/steamify.sh
dist/steamify-app.tar.gz
dist/steamify-app.sh
- name: Version
id: version
@@ -71,9 +86,13 @@ jobs:
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 assets/steam-gaming-settings.svg \
gh release create "v$VERSION" dist/steamify.sh dist/steamify-app.sh dist/steamify-app.tar.gz assets/steam-gaming-settings.svg \
--target "$GITHUB_SHA" --title "Steamify CachyOS v$VERSION" --notes "$notes" --latest
# "latest" follows the newest version tag, for the older install URL
@@ -86,4 +105,24 @@ jobs:
else
gh release create latest --title "Steamify CachyOS - latest (v$VERSION)" --notes "$latest_notes" --latest=false
fi
gh release upload latest dist/steamify.sh assets/steam-gaming-settings.svg --clobber
gh release upload latest dist/steamify.sh dist/steamify-app.sh dist/steamify-app.tar.gz assets/steam-gaming-settings.svg --clobber
- name: Publish the v2-ui-preview pre-release
if: github.event_name == 'push' && github.ref == 'refs/heads/feat/v2-ui'
env:
GH_TOKEN: ${{ github.token }}
run: |
# Its launcher downloads this pre-release, not the newest release.
sed -i 's/STEAMIFY_RELEASE:-latest/STEAMIFY_RELEASE:-v2-ui-preview/' dist/steamify-app.sh
grep -q 'STEAMIFY_RELEASE:-v2-ui-preview' dist/steamify-app.sh
git tag -f v2-ui-preview "$GITHUB_SHA" && git push -f origin v2-ui-preview
notes="Preview of the Steamify app (v2) from feat/v2-ui ($(git describe --always), $(git log -1 --format='%cs %s')). Not a release.
Run it with:
curl -fsSL https://github.com/${{ github.repository }}/releases/download/v2-ui-preview/steamify-app.sh | bash"
if gh release view v2-ui-preview >/dev/null 2>&1; then
gh release edit v2-ui-preview --title "Steamify app (v2) preview" --notes "$notes" --prerelease --latest=false
else
gh release create v2-ui-preview --target "$GITHUB_SHA" --title "Steamify app (v2) preview" --notes "$notes" --prerelease --latest=false
fi
gh release upload v2-ui-preview dist/steamify-app.sh dist/steamify-app.tar.gz --clobber
+1
View File
@@ -1 +1,2 @@
dist/
__pycache__/
+73 -4
View File
@@ -15,6 +15,10 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
- `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
`TECHNICAL.md`.
@@ -25,6 +29,14 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
`<id>_available` is checked via `component_available` (e.g. `machine`
only on Fremont). Turn-on order is `COMPONENTS` order, turn-off reverse;
`gaming` must stay first. Dependencies live in `toggle_component`.
- **Feature versions.** Set `FEATURE_VERSION[<id>]` (`lib/menu.sh`) to the new
`VERSION` whenever what `<id>_enable` sets up changes (a new component gets
one too): installs recorded with an older version
are ticked and re-applied ("update" in the plan and the app). New default
options are ticked for installs whose parent (top-level: `gaming`) is on
(`feature_new`, shown as "new").
Status still comes from the system; don't add ad-hoc `<id>_repair` checks
for new changes.
- **Reversibility.** Every per-user KDE setting a component changes goes
through `kset <component> <file> <group|group> <key> <value>`
(`lib/state.sh`), which records the old value once; `<id>_disable` calls
@@ -142,7 +154,7 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
`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`), not an on/off
- `bios` is an *action* (`ACTIONS` in `lib/menu.sh`) and a sub-option of `machine`, not an on/off
component: never preselected (not even on a first run), never re-applied
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
@@ -161,9 +173,14 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
now? [Y/n]" (`quit_prompt`): `n` goes back to the menu, the next `q` asks
again. Scripted input that runs out quits without restarting (never
restart on EOF: `ask_yn` would take its default).
- Steamify shortcut (`launcher`): the icon runs `curl | bash` of
`releases/latest/download/steamify.sh` in Konsole, so it's
always the newest release. Its icon, `assets/steam-gaming-settings.svg`
- 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
@@ -171,6 +188,58 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
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.
+155
View File
@@ -5,6 +5,161 @@ All notable changes, per version and per commit. Versions follow
`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**
+23 -12
View File
@@ -25,9 +25,15 @@ Steam Machine.
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** - an icon on the desktop and in the launcher that
opens the newest Steamify, so you don't need the install command again.
6. **HDMI-CEC** (experimental; off by default, on by default on a Steam
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
@@ -42,15 +48,12 @@ What Steamify can do on a Valve Steam Machine:
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.
- **Pin the kernel to 7.1.6-1** - on by default: with newer CachyOS kernels
the Steam Machine reboots instead of shutting down. Untick it once CachyOS
fixes that. The kernel comes from its own
[release](https://github.com/theupriser/steamify-cachyos/releases/tag/kernel-7.1.6-1),
checked against CachyOS's signature
([details](TECHNICAL.md#kernel-pin-steam-machine)).
- **Update BIOS** - installs Valve's newest Steam Machine BIOS. Never ticked
**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)).
@@ -124,3 +127,11 @@ Contributing (or an AI agent)? Read [AGENTS.md](AGENTS.md).
## License
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.
+87 -19
View File
@@ -192,6 +192,13 @@ devices found. Also turn on CEC on the TV
`steamos-manager` writes cecd's settings from Steam's (wake the TV, put it to
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,
@@ -201,26 +208,84 @@ upset CEC for the TV's other devices
Turning it off removes the packages again (`linuxconsole` only if the
wizard installed it).
## Kernel pin (Steam Machine)
## Power-off fix (Steam Machine)
With CachyOS kernels newer than 7.1.6 a Steam Machine reboots instead of
shutting down. The **Pin the kernel** sub-option (ticked along with Steam
Machine support) installs `linux-cachyos` and `linux-cachyos-headers`
7.1.6-1 and adds them to `IgnorePkg` in `/etc/pacman.conf`, so updates skip
them. DKMS builds the LED driver for it; restart to boot it.
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 packages (and their signatures, which pacman checks) are kept in
`/var/cache/steamify/kernel`, so re-applying needs no download. Missing
files are taken from pacman's cache, else downloaded from this repo's
`kernel-7.1.6-1` release, then `archive.cachyos.org`, then
`mirror.cachyos.org` (which only has the current kernel). Every file must
match the SHA-256 in the script and have a valid CachyOS signature; a bad
one is deleted, so the next run downloads it again. Set `PINNED_KERNEL_URL`
to a directory URL with the files to try another source first, or drop them
into the kernel directory yourself.
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.
Unticking it removes the pin and runs `sudo pacman -Syu`, which brings the
kernel back to CachyOS's current version; the files stay for next time.
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)
@@ -290,8 +355,11 @@ immediately, which can turn into a loop - see
| `lib/wizard-shortcut.sh` | Steamify shortcut: desktop icon and launcher entry that run the newest release |
| `lib/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; kernel pin |
| `.github/tools/bundle.sh` | Builds the single-file version (`dist/steamify.sh`) |
| `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
+230
View File
@@ -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
}
+74 -44
View File
@@ -121,54 +121,102 @@ bios_fits_device() {
grep -q '"Guid"' <<< "$details" && ! grep -q '"UpdateError"' <<< "$details"
}
bios_enable() {
# The checked firmware waits here between bios_prepare and bios_flash, so the
# app can show its warnings in between (and the terminal flow uses it too).
BIOS_STAGE_DIR="${XDG_RUNTIME_DIR:-/tmp}/steamify-bios"
bios_prepare() {
# Downloads Valve's package, checks its SHA-256 and extracts the .cab to
# $BIOS_STAGE_DIR, then asks fwupd whether it fits this machine. Sets
# BIOS_COMPATIBLE (yes|dry-run). 0 = ready to flash, 2 = nothing to do
# (already the newest), 1 = error (nothing was touched).
if ! bios_lookup_newest; then
err "Couldn't find the newest Steam Machine BIOS on Valve's mirror ($VALVE_MIRROR)."
return 1
fi
local current tmp compatible
current="$(bios_current)"
[[ -n "$BIOS_DRY_RUN" ]] && warn "DRY RUN (WIZARD_BIOS_DRY_RUN): nothing will be flashed."
if [[ "$current" == "$BIOS_NEWEST" ]]; then
ok "The BIOS is already the newest version ($current); nothing to do."
return 0
if [[ "$(bios_current)" == "$BIOS_NEWEST" ]]; then
ok "The BIOS is already the newest version ($BIOS_NEWEST); nothing to do."
return 2
fi
# Download and check everything before asking: the warnings only come
# when the file is Valve's (SHA-256 from Valve's repo database) and fwupd
# confirms it's firmware for this very machine.
pacman -Q fwupd >/dev/null 2>&1 || sudo pacman -S --needed --noconfirm fwupd ||
{ err "Installing fwupd failed."; return 1; }
tmp="$(mktemp -d)"
rm -rf "$BIOS_STAGE_DIR"; mkdir -p "$BIOS_STAGE_DIR"; chmod 700 "$BIOS_STAGE_DIR"
info "Downloading $BIOS_PKG ($BIOS_REPO)..."
if ! curl -fsSL "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_PKG" -o "$tmp/pkg.tar.zst" ||
! echo "$BIOS_SHA256 $tmp/pkg.tar.zst" | sha256sum -c --quiet - ||
! tar -I unzstd -xf "$tmp/pkg.tar.zst" -C "$tmp" "$BIOS_CAB"; then
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 "$tmp"
rm -rf "$BIOS_STAGE_DIR"
return 1
fi
ok "Checksum OK: this is Valve's $BIOS_PKG."
local yes="$c_green$c_bold" b="$c_bold" n="$c_reset"
if [[ -n "$BIOS_DRY_RUN" ]]; then
warn "Dry run: skipping fwupd's check that the firmware fits this machine."
compatible="${c_yellow}${b}not checked${n} (dry run)"
elif bios_fits_device "$tmp/$BIOS_CAB"; then
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."
compatible="${yes}yes${n} (checked by fwupd)"
BIOS_COMPATIBLE=yes
else
err "fwupd says BIOS $BIOS_NEWEST is not for this machine's hardware; not installing it."
rm -rf "$tmp"
rm -rf "$BIOS_STAGE_DIR"
return 1
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 "$tmp"; return 0
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" \
@@ -176,28 +224,10 @@ bios_enable() {
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 "$tmp"; return 0
info "BIOS update cancelled; nothing was changed."; rm -rf "$BIOS_STAGE_DIR"; return 0
fi
if [[ -n "$BIOS_DRY_RUN" ]]; then
ok "Dry run: would run: fwupdmgr install -y --no-reboot-check $(basename "$BIOS_CAB")"
ok "Dry run finished; nothing was flashed."
# Treated as staged, so the restart choices that follow a real
# update show up too; restarting only prints (see restart_now).
BIOS_NEEDS_RESTART=1
rm -rf "$tmp"
return 0
fi
info "Handing BIOS $BIOS_NEWEST to fwupd. Do NOT turn off the power from now on."
# -y: we already asked twice; --no-reboot-check: the wizard's own
# restart question comes at the end.
if ! sudo fwupdmgr install -y --no-reboot-check "$tmp/$BIOS_CAB"; then
err "fwupd could not install the BIOS update (see above); the BIOS was not changed."
rm -rf "$tmp"
return 1
fi
rm -rf "$tmp"
BIOS_NEEDS_RESTART=1
ok "BIOS $BIOS_NEWEST is staged. It is written during the next restart:"
warn "keep the power on and don't touch the machine until it has fully started again."
bios_flash
}
# For the app (lib/backend.sh): the flash step alone, after its warnings.
bios_flash_only() { bios_flash; }
+103 -14
View File
@@ -3,24 +3,105 @@
# 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 (e.g. the Steam Machine) or a USB adapter.
# On a Steam Machine, steamos-manager configures cecd from Steam's settings.
# 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
restart_steamos_manager() {
# steamos-manager only offers its HDMI-CEC interface (Steam's CEC
# settings) when cecd was there at its start.
systemctl --user is-active -q steamos-manager.service 2>/dev/null &&
systemctl --user restart steamos-manager.service
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_status() { pacman -Q "${CEC_PKGS[@]}" >/dev/null 2>&1; }
cec_installed() { pacman -Q "${CEC_PKGS[@]}" >/dev/null 2>&1; }
cec_driver_ok() { ! detect_valve_fremont || [[ -d "$CEC_DKMS_SRC" ]]; }
# On = installed, Steam told to show its CEC settings and, on a Steam
# Machine, the driver built. An install from before 1.1.3 (settings) or
# 2.0.3 (driver) lacks one; the menu then ticks it (see cec_repair).
cec_status() { cec_installed && [[ -f "$CEC_STEAM_DROPIN" ]] && cec_driver_ok; }
cec_reload_driver() {
# cecd keeps /dev/cec0 open, so the old module can't be unloaded under it.
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
@@ -63,6 +144,7 @@ cec_enable() {
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
@@ -70,12 +152,17 @@ cec_enable() {
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
systemctl --user enable steamos-manager-configure-cecd.service 2>/dev/null
cec_link_steamos_manager
if compgen -G "/dev/cec*" >/dev/null; then
systemctl --user restart cecd.service 2>/dev/null
restart_steamos_manager
ok "HDMI-CEC on ($(cd /dev && echo cec*)). Turn on CEC on your TV too (e.g. Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink)."
ok "HDMI-CEC on ($(cd /dev && echo cec*)); Steam shows its settings from the next gaming mode start."
info "Turn on CEC on your TV too (e.g. Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink)."
else
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*"
@@ -87,15 +174,17 @@ cec_disable() {
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
sudo rm -f "$CEC_ORDER_DROPIN"
sudo rmdir "$(dirname "$CEC_ORDER_DROPIN")" 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
restart_steamos_manager
systemctl --user is-active -q steamos-manager.service 2>/dev/null &&
systemctl --user restart steamos-manager.service
ok "HDMI-CEC removed."
}
+3
View File
@@ -6,6 +6,9 @@ c_reset="\033[0m"; c_bold="\033[1m"; c_green="\033[32m"; c_yellow="\033[33m"; c_
info() { echo -e "${c_cyan}[INFO]${c_reset} $*"; }
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() {
+70
View File
@@ -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."
}
+696
View File
@@ -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."
}
+108 -19
View File
@@ -5,17 +5,30 @@
# Menu order. Components are turned on in this order and off in reverse;
# gaming must come first (single user builds on it).
COMPONENTS=(gaming boot theme glyphs single launcher cec machine kpin bios)
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 [kpin]=machine)
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.
NO_PRESELECT=(boot cec)
# 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"
@@ -23,17 +36,23 @@ declare -A LABEL=(
[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: desktop icon to run this again"
[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"
[kpin]="Pin the kernel to $PINNED_KERNEL_VER (fixes rebooting after shutdown)"
[poweroff]="Power-off fix: the Steam Machine stays off after shutting down"
[kpin]="Pin the kernel to $PINNED_KERNEL_VER (untick for CachyOS's current kernel)"
[hdmi]="HDMI refresh boost: highest refresh your HDMI display runs"
[bios]="Update BIOS"
)
declare -A CURRENT WANTED
component_available() {
case "$1" in
machine|kpin) machine_available ;;
machine|poweroff) machine_available ;;
vram) vram_available ;;
kpin) kpin_available ;;
hdmi) hdmi_available ;;
bios) bios_available ;;
esac
}
@@ -53,10 +72,52 @@ menu_visible() {
[[ -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
}
@@ -67,12 +128,30 @@ detect_components() {
if "${c}_status"; then CURRENT[$c]=1; any=true; else CURRENT[$c]=0; fi
WANTED[$c]=${CURRENT[$c]}
done
# HDMI-CEC set up by an older version: tick it, so a normal run fixes it.
component_available cec && cec_repair && WANTED[cec]=1
# Updated features and new default sub-options: ticked, so a normal run
# applies them.
for c in "${COMPONENTS[@]}"; do
component_available "$c" || continue
{ feature_outdated "$c" || feature_new "$c"; } && WANTED[$c]=1
done
# The kernel pin is no longer needed (the power-off fix), and HDMI
# refresh boost needed the pin: whatever is left of either is unticked,
# so a normal run removes it.
component_available kpin && WANTED[kpin]=0
component_available hdmi && WANTED[hdmi]=0
# The terminal-only Steamify shortcut from before 2.0.1: tick it, so a
# normal run replaces it with the app.
launcher_repair && WANTED[launcher]=1
# Shows the current and newest BIOS version.
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" && ! is_action "$c" &&
component_available "$c" && component_selectable "$c" && ! is_action "$c" &&
[[ " ${NO_PRESELECT[*]} " != *" $c "* ]] && WANTED[$c]=1
done
machine_available && WANTED[cec]=1
@@ -88,9 +167,17 @@ toggle_component() {
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 kernel pin is opt-out: ticked along with Steam Machine support.
if [[ "$c" == machine ]]; then WANTED[kpin]=${WANTED[machine]}; 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() {
@@ -113,12 +200,13 @@ show_menu() {
[[ "${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 └ %s\n" "$i" "$now" "$want" "${LABEL[$c]}"
elif component_selectable "$c"; then
printf " %-3s %b %-6s %s\n" "$i" "$now" "$want" "${LABEL[$c]}"
printf " %-3s %b %-6s └ %b\n" "$i" "$now" "$want" "$(menu_label "$c")"
else
printf " %b%-3s %-6s %-6s %s (not available)%b\n" "$c_dim" "$i" "$now" "$want" "${LABEL[$c]}" "$c_reset"
printf " %-3s %b %-6s %b\n" "$i" "$now" "$want" "$(menu_label "$c")"
fi
done
echo
@@ -166,8 +254,9 @@ draw_menu_tui() {
fi
if ! component_selectable "$c"; then
# Greyed out: nothing to do (e.g. BIOS already up to date).
local mark=" "; (( i == cursor )) && mark=" ${c_cyan}>${c_reset} "
echo -e "${mark}${c_dim}[ ] ${LABEL[$c]} (not available)${c_reset}"
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}"
@@ -250,7 +339,7 @@ plan_changes() {
component_available "$c" || continue
[[ "${WANTED[$c]}" == 1 ]] || continue
if is_action "$c"; then TO_ENABLE+=("$c"); continue; fi
if [[ "${CURRENT[$c]}" == 0 || "$REAPPLY" == true ]] ||
if [[ "${CURRENT[$c]}" == 0 || "$REAPPLY" == true ]] || feature_outdated "$c" ||
[[ "$c" == gaming && "${CURRENT[single]}" != "${WANTED[single]}" ]]; then
TO_ENABLE+=("$c")
fi
@@ -265,13 +354,13 @@ apply_changes() {
for c in "${TO_DISABLE[@]}"; do
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
"${c}_disable" || failed+=("$c")
if "${c}_disable"; then feature_record "$c" disable; else failed+=("$c"); fi
done
for c in "${TO_ENABLE[@]}"; do
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
"${c}_enable" || failed+=("$c")
if "${c}_enable"; then is_action "$c" || feature_record "$c" enable; else failed+=("$c"); fi
done
FAILED=("${failed[@]}")
}
+9
View File
@@ -81,6 +81,13 @@ single_wallet_disable() {
ok "Your own wallet is back (the empty one is kept as kdewallet.kwl$WALLET_OURS)."
}
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
@@ -114,6 +121,7 @@ single_enable() {
single_launcher
restart_plasmashell_if_stopped
reload_screen_locker
single_wallet_enable
ok "Single user: no lock screen, user switching or log out."
}
@@ -143,6 +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."
}
+21 -10
View File
@@ -1,6 +1,7 @@
#!/bin/bash
# "Steam Machine support" menu item, only on Valve Fremont hardware: the
# front LED bar driver, LED access for Steam, and steamos-manager.
# front LED bar driver, LED access for Steam and steamos-manager. Its
# sub-option "Power-off fix" is lib/fremont-poweroff.sh.
# Sourced by steamify.sh; not meant to be run on its own.
detect_valve_fremont() {
@@ -14,8 +15,8 @@ detect_valve_fremont() {
[[ "$vendor" == "OEM" && "$product" == "F7F" ]]
}
# Kernel pinned on a Steam Machine: with newer linux-cachyos releases it
# reboots instead of shutting down. The packages are kept in
# 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"
@@ -148,7 +149,9 @@ remove_kernel_pin() {
RESTART_FOR_LOGIN=true
}
# "Pin the kernel" menu item, a sub-option of Steam Machine support.
# "Pin the kernel" menu item, a sub-option of Steam Machine support. The
# power-off fix made it unnecessary: only shown while on, to remove it.
kpin_available() { detect_valve_fremont && kpin_status; }
kpin_status() {
pinned_kernel_installed && grep -Eq '^IgnorePkg\s*=.*\slinux-cachyos(\s|$)' /etc/pacman.conf
}
@@ -163,7 +166,12 @@ kpin_enable() {
fi
}
kpin_disable() { remove_kernel_pin; }
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
@@ -329,13 +337,13 @@ reload_powerdevil() {
kernel_overview() {
# Per installed kernel: headers (needed to build DKMS modules), the
# in-kernel Steam controller driver, and, on a Steam Machine, whether the
# LED driver is built for it. Printed in the menu so it's easy to verify
# LED driver and the power-off fix are built for it. Printed in the menu so it's easy to verify
# a kernel update or a newly added kernel got everything.
local mark="${c_green}yes${c_reset}" miss="${c_red}no ${c_reset}" leds=false
detect_valve_fremont && command -v dkms >/dev/null 2>&1 && leds=true
local kdir k pkg headers hid led running line
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"
[[ "$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")"
@@ -347,11 +355,12 @@ kernel_overview() {
line="$(printf '%-23s %-18s headers %b controller %b' "$k" "$pkg" "$headers" "$hid")"
if [[ "$leds" == true ]]; then
led="$miss"; dkms status -k "$k" leds-valve-dkms 2>/dev/null | grep -q installed && led="$mark"
line+="$(printf ' LEDs %b' "$led")"
off="$miss"; dkms status -k "$k" "$POWEROFF_DKMS_NAME" 2>/dev/null | grep -q installed && off="$mark"
line+="$(printf ' LEDs %b power-off %b' "$led" "$off")"
fi
echo -e " ${running}${line}"
done
cec_status && { echo; echo -e " $(cec_overview)"; }
cec_installed && { echo; echo -e " $(cec_overview)"; }
if [[ "$leds" == true ]]; then
local loaded="$miss" nodes
lsmod | grep -q '^leds_valve' && loaded="$mark"
@@ -395,6 +404,8 @@ EOF
sudo systemctl enable --now inputplumber.service
sudo systemctl enable --now steamos-manager.service
systemctl --user enable steamos-manager.service 2>/dev/null
# HDMI-CEC ran before this and couldn't link cecd to steamos-manager yet.
cec_status && cec_link_steamos_manager
# Console-like power handling, as SteamOS's powerdevilrc (mains-power
# part; there is no battery): the power button sleeps instead of showing
+78
View File
@@ -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."
}
+61 -14
View File
@@ -1,19 +1,29 @@
#!/bin/bash
# "Steamify shortcut" menu item: a desktop icon and an app launcher entry that
# open the newest release of Steamify CachyOS in Konsole (curl | bash, like the
# install command in the README), so it's never out of date.
# "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_DESKTOP_NAME="cachyos-gamescope-boot-wizard.desktop"
WIZARD_APP_ENTRY="${XDG_DATA_HOME:-$HOME/.local/share}/applications/$WIZARD_DESKTOP_NAME"
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_file() {
echo "$(xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop")/$WIZARD_DESKTOP_NAME"
wizard_desktop_dir() {
xdg-user-dir DESKTOP 2>/dev/null || echo "$HOME/Desktop"
}
install_executable() {
@@ -29,11 +39,15 @@ install_executable() {
}
launcher_status() {
[[ -x "$WIZARD_LAUNCHER" && -f "$WIZARD_APP_ENTRY" ]]
[[ -x "$WIZARD_APP_LAUNCHER" && -x "$WIZARD_LAUNCHER" && -f "$WIZARD_APP_ENTRY" && -f "$WIZARD_TERMINAL_ENTRY" ]]
}
# A shortcut from before 2.0.1 (terminal only): the menu ticks the item, so a
# normal run replaces it.
launcher_repair() { [[ -f "$WIZARD_APPS/$WIZARD_OLD_NAME" ]] && ! launcher_status; }
launcher_enable() {
info "Adding the Steamify shortcut (desktop icon and app launcher entry)..."
info "Adding the Steamify shortcut (the app on the desktop and in the launcher, Steamify Terminal in the launcher)..."
install_executable "$WIZARD_LAUNCHER" 755 << EOF
#!/bin/bash
# Installed by Steamify CachyOS: run its newest release.
@@ -53,6 +67,23 @@ for (( i = 10; i > 0; i-- )); do
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.
@@ -72,18 +103,34 @@ EOF
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;"
printf '%s\n' "$entry" | install_executable "$WIZARD_APP_ENTRY" 644
printf '%s\n' "$entry" | install_executable "$(wizard_desktop_file)" 755
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\" on the desktop and in the launcher."
ok "Steamify shortcut added: \"Steamify CachyOS\" (the app) on the desktop and in the launcher, \"Steamify Terminal\" in the launcher."
}
launcher_disable() {
rm -f "$(wizard_desktop_file)" "$WIZARD_APP_ENTRY" "$WIZARD_LAUNCHER" "$WIZARD_ICON"
# The launcher entry only if it's ours: steamify-app.sh writes its own
# under the same name when there's no shortcut.
grep -q '^X-Steamify-Shortcut=true' "$WIZARD_APP_ENTRY" 2>/dev/null && rm -f "$WIZARD_APP_ENTRY"
rm -f "$(wizard_desktop_dir)/$WIZARD_DESKTOP_NAME" "$WIZARD_TERMINAL_ENTRY" \
"$(wizard_desktop_dir)/$WIZARD_OLD_NAME" "$WIZARD_APPS/$WIZARD_OLD_NAME" \
"$WIZARD_LAUNCHER" "$WIZARD_APP_LAUNCHER" "$WIZARD_ICON"
rmdir "$(dirname "$WIZARD_LAUNCHER")" 2>/dev/null || true
kbuildsycoca6 >/dev/null 2>&1 || true
ok "Steamify shortcut removed."
+10
View File
@@ -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 |
+21
View File
@@ -0,0 +1,21 @@
Steam Machine CEC: find amdgpu's HDMI port with a single CEC port.
amdgpu registers its HDMI notifier without a port name, which only matches
the driver's named lookup ("Port C") if the CEC driver loaded first; amdgpu
loads from the initramfs, so it never does and /dev/cec0 stays at f.f.f.f.
With a single CEC port, look the notifier up by device alone.
Applies to Valve's drivers/media/cec/platform/cros-ec/cros-ec-cec.c
(evlaV/linux-integration 10c8c880, see CEC_DRIVER_URL in lib/cec.sh).
--- a/cros-ec-cec.c
+++ b/cros-ec-cec.c
@@ -485,7 +485,7 @@
goto out_probe_adapter;
}
- port->notify = cec_notifier_cec_adap_register(hdmi_dev, conns[port_num],
+ port->notify = cec_notifier_cec_adap_register(hdmi_dev, conns[1] ? conns[port_num] : NULL,
port->adap);
if (!port->notify) {
ret = -ENOMEM;
+80
View File
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Steam Machine (Fremont): power off instead of booting up again.
*
* Since "pinctrl-amd: Don't clear S4 wake bits at probe" (Linux 7.2, also
* backported to 6.x, 7.0 and 7.1 updates) the S4/S5 wake bit the firmware
* leaves set on GPIO pin 18 (_SB.PCI0.GPP6) stays set, and the machine starts again right after powering off.
* Valve's kernel clears that bit at probe on Fremont (not for upstream);
* this does the same right before power-off, for kernels without it.
*/
#include <linux/acpi.h>
#include <linux/bits.h>
#include <linux/dmi.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/reboot.h>
#define FREMONT_GPP6_PIN 18
#define WAKE_CNTRL_OFF_S4 15
static void __iomem *pin_reg;
static struct sys_off_handler *handler;
static int fremont_poweroff_prepare(struct sys_off_data *data)
{
u32 v = readl(pin_reg);
if (v & BIT(WAKE_CNTRL_OFF_S4))
writel(v & ~BIT(WAKE_CNTRL_OFF_S4), pin_reg);
return NOTIFY_DONE;
}
static int __init fremont_poweroff_init(void)
{
struct acpi_device *adev;
struct device *dev;
struct resource *res;
u32 v;
if (!dmi_match(DMI_BOARD_NAME, "Fremont"))
return -ENODEV;
adev = acpi_dev_get_first_match_dev("AMDI0030", NULL, -1);
if (!adev)
return -ENODEV;
dev = bus_find_device_by_acpi_dev(&platform_bus_type, adev);
acpi_dev_put(adev);
if (!dev)
return -ENODEV;
res = platform_get_resource(to_platform_device(dev), IORESOURCE_MEM, 0);
put_device(dev);
if (!res || resource_size(res) < (FREMONT_GPP6_PIN + 1) * 4)
return -ENODEV;
/* pinctrl-amd owns the region; only this one register is touched. */
pin_reg = ioremap(res->start + FREMONT_GPP6_PIN * 4, 4);
if (!pin_reg)
return -ENOMEM;
v = readl(pin_reg);
pr_info("GPIO %d register 0x%08x, S4/S5 wake %s\n", FREMONT_GPP6_PIN, v,
v & BIT(WAKE_CNTRL_OFF_S4) ? "set (cleared at power-off)" : "clear");
handler = register_sys_off_handler(SYS_OFF_MODE_POWER_OFF_PREPARE,
SYS_OFF_PRIO_DEFAULT, fremont_poweroff_prepare, NULL);
if (IS_ERR(handler)) {
iounmap(pin_reg);
return PTR_ERR(handler);
}
return 0;
}
static void __exit fremont_poweroff_exit(void)
{
unregister_sys_off_handler(handler);
iounmap(pin_reg);
}
module_init(fremont_poweroff_init);
module_exit(fremont_poweroff_exit);
MODULE_DESCRIPTION("Steam Machine: clear GPIO 18's S4 wake bit before power-off");
MODULE_LICENSE("GPL");
MODULE_ALIAS("dmi:*:rnFremont:*");
+23 -6
View File
@@ -15,20 +15,25 @@
set -uo pipefail
# Release version, see CHANGELOG.md.
VERSION=1.1.1
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 cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu; do
for lib in common state packages login-manager single-user steam-desktop steam-machine fremont-poweroff vram-booster hdmi-refresh 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
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."
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."
@@ -98,6 +103,15 @@ quit_prompt() {
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
@@ -125,7 +139,10 @@ while true; do
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 [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}"; else echo " - turn on: ${LABEL[$c]}"; fi
elif feature_outdated "$c"; then echo " - update: ${LABEL[$c]} (changed in this version)"
elif [[ "${CURRENT[$c]}" == 1 ]]; then echo " - re-apply: ${LABEL[$c]}"
elif feature_new "$c"; then echo " - turn on: ${LABEL[$c]} (new in this version)"
else echo " - turn on: ${LABEL[$c]}"; fi
done
ask_yn "Go ahead?" y || { info "Nothing changed."; continue; }
+1142
View File
File diff suppressed because it is too large. Load diff
+74
View File
@@ -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
View File
@@ -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())