mirror of
https://github.com/theupriser/steamify-cachyos.git
synced 2026-10-03 17:41:58 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
febdab5b53 | ||
|
|
90cdfd6666 | ||
|
|
6f598d18fd | ||
|
|
9bbe5fa602 | ||
|
|
ceeb949e6b | ||
|
|
03a8b8c4ba | ||
|
|
e699a3f8eb | ||
|
|
7bef834b16 | ||
|
|
86b9744227 | ||
|
|
b0947d7a2f | ||
|
|
3f97c85c3c | ||
|
|
fd162c1e2e | ||
|
|
54ebddfe98 | ||
|
|
fe93dd3e67 | ||
|
|
ef585c0db1 | ||
|
|
32795bd5a5 | ||
|
|
3c8ca65902 | ||
|
|
11a91ef85c | ||
|
|
163f7908c4 | ||
|
|
e121b06fd8 | ||
|
|
19ca966913 | ||
|
|
56fb9080a7 | ||
|
|
6df19eba3e | ||
|
|
7694c97d68 | ||
|
|
a5e644d71a | ||
|
|
929ecfb349 | ||
|
|
4cc87aaea6 | ||
|
|
36d0a9d780 |
No files matched your search
@@ -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.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# 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/, the icon) and its launcher, run with:
|
||||
# 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:
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
.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 ui assets/steam-gaming-settings.svg LICENSE.md
|
||||
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
|
||||
|
||||
@@ -15,6 +15,17 @@ 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.
|
||||
- `ui/` - the app: `steamify-ui` (PySide6; runs `steamify.sh --backend`,
|
||||
`lib/backend.sh`) and `ui/qml/`: `Main.qml` (window, header, which screen
|
||||
shows), `AppState.qml` (all state and logic, input actions), one
|
||||
`*Screen.qml` per screen, small widgets (`Btn`, `Chip`, `Badge`, ...), and
|
||||
the singletons `Theme` (colours, fonts), `Texts` (item texts) and `Input`
|
||||
(controller/keyboard/remote and its button names), listed in `qmldir`.
|
||||
Screens get the `AppState` as `app` and only show it or call its functions.
|
||||
- `lib/*.sh` - one file per responsibility, each defining functions only
|
||||
(no top-level side effects besides constants). See the table in
|
||||
`TECHNICAL.md`.
|
||||
@@ -25,6 +36,14 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
||||
`<id>_available` is checked via `component_available` (e.g. `machine`
|
||||
only on Fremont). Turn-on order is `COMPONENTS` order, turn-off reverse;
|
||||
`gaming` must stay first. Dependencies live in `toggle_component`.
|
||||
- **Feature versions.** Set `FEATURE_VERSION[<id>]` (`lib/menu.sh`) to the new
|
||||
`VERSION` whenever what `<id>_enable` sets up changes (a new component gets
|
||||
one too): installs recorded with an older version
|
||||
are ticked and re-applied ("update" in the plan and the app). New default
|
||||
options are ticked for installs whose parent (top-level: `gaming`) is on
|
||||
(`feature_new`, shown as "new").
|
||||
Status still comes from the system; don't add ad-hoc `<id>_repair` checks
|
||||
for new changes.
|
||||
- **Reversibility.** Every per-user KDE setting a component changes goes
|
||||
through `kset <component> <file> <group|group> <key> <value>`
|
||||
(`lib/state.sh`), which records the old value once; `<id>_disable` calls
|
||||
@@ -176,18 +195,10 @@ 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. The kernel command-line value holds a path, so
|
||||
`sed` uses `|` as its delimiter.
|
||||
- HDMI refresh boost (`hdmi`, `lib/hdmi-refresh.sh`): retired in 2.2.0
|
||||
(it needed the pinned kernel); only removal is left (see the power-off
|
||||
fix below). `hdmi_enable` refuses. debugfs is root-only (glob it under
|
||||
sudo), and `edid_override` takes exactly `reset` with no newline.
|
||||
- Steam Machine CEC driver (`cec_driver_enable`, `lib/cec.sh`): mainline
|
||||
`cros_ec_cec` lacks Fremont, so HDMI-CEC builds Valve's copy (evlaV
|
||||
`linux-integration`, pinned commit + SHA-256) with DKMS for every kernel.
|
||||
@@ -196,6 +207,30 @@ gamescope and the Plasma desktop. Primary target: the Valve Steam Machine
|
||||
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.
|
||||
|
||||
|
||||
@@ -5,6 +5,87 @@ 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.4.1 - 2026-09-27
|
||||
|
||||
- **chore: Version 2.4.1**
|
||||
- `6f598d1` **docs: README screenshot shows every option**
|
||||
- `9bbe5fa` **docs: Screenshot of the app's menu in the README**
|
||||
|
||||
## 2.4.0 - 2026-09-27
|
||||
|
||||
- `03a8b8c` **chore: Version 2.4.0**
|
||||
- `e699a3f` **refactor(ui): One QML file per screen; the HDMI boost's setup screens and backend commands removed**
|
||||
|
||||
## 2.3.0 - 2026-09-27
|
||||
|
||||
- `86b9744` **feat: VRAM booster greyed out with an NVIDIA card, with why**
|
||||
- `b0947d7` **feat: VRAM booster only for GPUs with at least 2 GB of their own VRAM**
|
||||
- `3f97c85` **feat: VRAM booster for every supported GPU, not only the Steam Machine**
|
||||
- `fd162c1` **feat: New badge for default sub-options added in this version**
|
||||
- `54ebddf` **feat: VRAM booster for the Steam Machine**
|
||||
|
||||
## 2.2.0 - 2026-09-27
|
||||
|
||||
- `ef585c0` **docs: Changelog entries per commit for 2.2.0**
|
||||
- `32795bd` **feat(ui): Update badge on the progress screen**
|
||||
- `3c8ca65` **fix(ui): Never write the sudo password to a file; Update label on the progress screen**
|
||||
- The app gave `sudo` the password through a short-lived file; the helper
|
||||
now reads it from a named pipe the app feeds from memory each time sudo
|
||||
asks. The progress screen showed "undefined" for an update.
|
||||
- `11a91ef` **feat: Update badge on components that get a newer version**
|
||||
- The menu (terminal and app) shows "(update)" / an Update badge after
|
||||
the name of a component a normal run will update.
|
||||
- `163f790` **feat: Feature versions are Steamify versions; Steam Machine support is 2.2.0**
|
||||
- Each component records the Steamify version of what it set up; when a
|
||||
release changes a component, installs with an older version are ticked
|
||||
and updated by a normal run ("update" in the plan and the app's
|
||||
review). A setup from before counts as 2.1.0, so Steam Machine support
|
||||
(2.2.0) is updated on machines set up with 2.1.0. New default
|
||||
sub-options (like the power-off fix) are ticked where their parent is on.
|
||||
- `e121b06` **fix: Remove whatever is left of HDMI refresh boost, pin or no pin**
|
||||
- HDMI refresh boost needed the pinned kernel and is retired (newer
|
||||
kernels do HDMI 2.1 themselves): whatever is left of it (saved
|
||||
displays, also ones not connected, its hotplug script, the old kernel
|
||||
parameter) is unticked and removed by the next run.
|
||||
- `19ca966` **docs: Drop the kernel pin and HDMI refresh boost descriptions**
|
||||
- `56fb908` **docs: Power-off fix texts cover every affected kernel, not just 7.2**
|
||||
- `6df19eb` **feat: Power-off fix as its own sub-option, drop the kernel pin, feature versions**
|
||||
- The power-off fix is a sub-option of Steam Machine support, ticked
|
||||
along with it (opt-out). The kernel pin is no longer offered: an
|
||||
existing pin is shown unticked, so the next run removes it and brings
|
||||
back CachyOS's current kernel.
|
||||
- `7694c97` **feat: Power-off fix for the Steam Machine; the kernel pin is optional**
|
||||
- With recent kernels (newer 6.x, 7.0 and 7.1 updates, 7.2 and probably
|
||||
later ones) the Steam Machine started again right after shutting down:
|
||||
they keep the wake bit the firmware sets on GPIO pin 18, which Valve's
|
||||
own kernel clears in a patch that won't reach CachyOS. A small module,
|
||||
built with DKMS for each installed kernel, clears it right before
|
||||
power-off. Tested with CachyOS 7.2.8: stays off, and without the module
|
||||
it rebooted.
|
||||
- Module sources and patches live in `patches/` instead of inside the
|
||||
shell code; the single-file build and the app package include them. The
|
||||
Steam Machine CEC driver change is now
|
||||
`patches/cros-ec-cec-single-port.patch`.
|
||||
|
||||
## 2.1.0 - 2026-09-26
|
||||
|
||||
- **feat(ui): Manage button for HDMI refresh boost**
|
||||
- The item's switch is a button: Set up… while no display is saved,
|
||||
Manage after that. Manage lists the saved displays (connected or not),
|
||||
removes one with its Remove button, and sets up the connected display
|
||||
when it has no saved rates yet.
|
||||
- **4cc87aa feat: HDMI refresh boost per display, with a Saved displays screen**
|
||||
- **36d0a9d feat: HDMI refresh boost only for the display it was set up for**
|
||||
- The EDID override was on the kernel command line, so any display on that
|
||||
HDMI port got the first display's timings. Rates are now saved per
|
||||
display and loaded at boot and at every hotplug for the display that's
|
||||
connected (its ID read over DDC); a display without saved rates, or
|
||||
unplugging, puts the port back on the display's own EDID. No initramfs
|
||||
or boot loader change any more. An existing setup is moved over when
|
||||
re-applied (`a`).
|
||||
- The item shows on only while the connected display is boosted, so a new
|
||||
display can be set up by ticking it; unticking removes only that one.
|
||||
|
||||
## 2.0.3 - 2026-09-26
|
||||
|
||||
- **fix: HDMI-CEC on the Steam Machine**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Steamify CachyOS
|
||||
|
||||

|
||||
|
||||
Turn a CachyOS desktop PC into a SteamOS-style console: it boots straight
|
||||
into Steam's Big Picture (gamescope), and you can switch to the KDE Plasma
|
||||
desktop and back whenever you like - just like on a Steam Deck or Valve's
|
||||
@@ -28,7 +30,12 @@ Steam Machine.
|
||||
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. **HDMI-CEC** (experimental; off by default, on by default on a Steam
|
||||
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
|
||||
@@ -43,20 +50,11 @@ 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)).
|
||||
- **HDMI refresh boost** - only with the pinned kernel, never ticked by
|
||||
default: gets the highest refresh rate your HDMI display runs at the
|
||||
desktop resolution (e.g. 110 Hz instead of 60 on a 3440x1440 monitor).
|
||||
Each step is shown and you confirm it; no answer within 15 s switches
|
||||
back. For more (e.g. 175 Hz), use DisplayPort
|
||||
([details](TECHNICAL.md#hdmi-refresh-boost-steam-machine)).
|
||||
**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)).
|
||||
|
||||
+74
-56
@@ -208,68 +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.
|
||||
|
||||
## HDMI refresh boost (Steam Machine)
|
||||
## VRAM booster
|
||||
|
||||
With the pinned kernel (7.1.6), HDMI displays often stay at 60 Hz. Two
|
||||
reasons:
|
||||
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:
|
||||
|
||||
- Monitors list their fast modes in an extra EDID block, announced by the
|
||||
HDMI Forum EEODB data block. 7.1.6 only reads the first extension block,
|
||||
so it never sees them. Newer kernels do.
|
||||
- Their fastest modes need HDMI 2.1 (FRL). 7.1.6's amdgpu only does HDMI
|
||||
2.0 (TMDS, at most 600 MHz), but a mode with the display's own shortest
|
||||
blanking at a slightly lower rate often fits.
|
||||
- `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).
|
||||
|
||||
The menu item (only on a Steam Machine with the pinned kernel, never
|
||||
ticked by default, run from the desktop in Konsole):
|
||||
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.
|
||||
|
||||
1. Takes the desktop resolution from KDE (`kscreen-doctor -j`) and reads the
|
||||
display's complete EDID over DDC (`i2ctransfer`, segment pointer 0x30).
|
||||
A live EDID left by an earlier test is cleared first.
|
||||
2. Calculates the highest rate that fits: the display's TMDS limit (HDMI
|
||||
Forum VSDB, capped at amdgpu's 600 MHz), its shortest blanking at that
|
||||
resolution and its maximum refresh (range limits, VRR maximum). Steps:
|
||||
that rate rounded down to ten, and the hundred below it as a safe option.
|
||||
Rates the display already lists, or that aren't faster than what works
|
||||
now, are left out.
|
||||
3. Builds the EDID: all of the display's blocks, the block count and EEODB
|
||||
fixed, plus a DisplayID block with the steps.
|
||||
4. Loads it live (debugfs `edid_override`, `trigger_hotplug`) and switches
|
||||
to each step, lowest first. Each one needs a "y" within 15 s
|
||||
(`WIZARD_HDMI_CONFIRM_SECONDS` for tests); anything else switches back
|
||||
and stops.
|
||||
5. Makes it permanent with only the confirmed steps:
|
||||
`/usr/lib/firmware/edid/steamify-<connector>.bin`, in the initramfs via
|
||||
`/etc/mkinitcpio.conf.d/90-steamify-edid.conf` (amdgpu loads from there),
|
||||
and `drm.edid_firmware=<connector>:edid/steamify-<connector>.bin` on the
|
||||
kernel command line: `/etc/default/limine` (`limine-mkinitcpio`),
|
||||
`/etc/sdboot-manage.conf` or `/etc/default/grub`.
|
||||
## Feature versions (updates)
|
||||
|
||||
The display's manufacturer, model and serial are stored; re-applying with
|
||||
another display connected tests that one instead. Turning it off removes
|
||||
the files and the parameter and rebuilds. Untick it before removing the
|
||||
kernel pin: newer kernels read the EDID themselves and can do HDMI 2.1.
|
||||
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)
|
||||
|
||||
@@ -339,9 +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 |
|
||||
| `lib/hdmi-refresh.sh` | HDMI refresh boost (Steam Machine, pinned kernel): EDID over DDC, calculated steps, live test, `drm.edid_firmware` |
|
||||
| `.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
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 484 KiB |
+12
-35
@@ -8,14 +8,6 @@
|
||||
# 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>
|
||||
# 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).
|
||||
@@ -60,6 +52,8 @@ backend_status() {
|
||||
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
|
||||
@@ -135,7 +129,6 @@ backend_apply() {
|
||||
case "$1" in
|
||||
--reapply) reapply=true ;;
|
||||
--boot) boot="$2"; shift ;;
|
||||
--hdmi) HDMI_CHOICE+=("$2"); shift ;;
|
||||
*) want+=("$1") ;;
|
||||
esac
|
||||
shift
|
||||
@@ -151,10 +144,12 @@ backend_apply() {
|
||||
# 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
|
||||
# Retired: only ever removed.
|
||||
WANTED[hdmi]=0
|
||||
case "$boot" in desktop) WANTED[boot]=1; WANTED[gaming]=1 ;; gamescope) WANTED[boot]=0 ;; esac
|
||||
|
||||
REAPPLY=$reapply
|
||||
@@ -170,40 +165,22 @@ backend_apply() {
|
||||
LOGIN_MANAGER=plasmalogin
|
||||
[[ "${WANTED[single]}" == 1 ]] && LOGIN_MANAGER=sddm
|
||||
local -a failed=()
|
||||
for c in "${TO_DISABLE[@]}"; do backend_run_component "$c" disable || failed+=("$c"); done
|
||||
for c in "${TO_ENABLE[@]}"; do backend_run_component "$c" enable || failed+=("$c"); done
|
||||
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 [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 ;;
|
||||
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 "$@" ;;
|
||||
*) err "Usage: steamify.sh --backend status | apply [--reapply] [--boot gamescope|desktop] <id>..."; return 2 ;;
|
||||
esac
|
||||
}
|
||||
+4
-9
@@ -60,8 +60,8 @@ cec_reload_driver() {
|
||||
cec_driver_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; }
|
||||
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)"
|
||||
@@ -72,13 +72,8 @@ cec_driver_enable() {
|
||||
err "Downloading Valve's CEC driver failed (or its checksum didn't match)."
|
||||
return 1
|
||||
fi
|
||||
# amdgpu registers its HDMI notifier without a port name, which only
|
||||
# matches the driver's named lookup ("Port C") if the driver loaded
|
||||
# first; amdgpu loads from the initramfs, so it never does. With a
|
||||
# single CEC port, look it up by device alone.
|
||||
sed -i 's/cec_notifier_cec_adap_register(hdmi_dev, conns\[port_num\],/cec_notifier_cec_adap_register(hdmi_dev, conns[1] ? conns[port_num] : NULL,/' \
|
||||
"$tmp/cros-ec-cec.c"
|
||||
grep -q 'conns\[1\] ? conns\[port_num\] : NULL' "$tmp/cros-ec-cec.c" ||
|
||||
# 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." \
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
+52
-508
@@ -1,27 +1,22 @@
|
||||
#!/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 makes it permanent with an EDID override
|
||||
# (drm.edid_firmware). 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 refresh boost" menu item (hdmi), retired in 2.2.0: it saved an EDID
|
||||
# override per display (with a hotplug script loading it) so the pinned
|
||||
# 7.1.6 kernel ran HDMI displays above 60 Hz. Newer kernels read the whole
|
||||
# EDID themselves, and the kernel pin is gone with the power-off fix, so
|
||||
# only its removal is left: shown while anything of it is on the system,
|
||||
# always unticked, so a normal run removes it.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
HDMI_FW_DIR=/usr/lib/firmware/edid
|
||||
# Before 2.1.0 the override was on the kernel command line.
|
||||
HDMI_INITRAMFS_CONF=/etc/mkinitcpio.conf.d/90-steamify-edid.conf
|
||||
# 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=()
|
||||
# The saved displays; their EDIDs are $HDMI_FW_DIR/steamify-<id>.bin.
|
||||
HDMI_MAP=/etc/steamify/hdmi-edid.conf
|
||||
HDMI_RUN=/run/steamify-edid
|
||||
HDMI_HOTPLUG=/usr/local/bin/steamify-edid-hotplug
|
||||
HDMI_UNIT_NAME=steamify-edid.service
|
||||
HDMI_UNIT="/etc/systemd/system/$HDMI_UNIT_NAME"
|
||||
HDMI_UDEV_RULE=/etc/udev/rules.d/90-steamify-edid.rules
|
||||
|
||||
hdmi_connectors() {
|
||||
# Connected HDMI outputs, as "<card>-<connector>" (e.g. card1-HDMI-A-1).
|
||||
@@ -45,300 +40,35 @@ hdmi_cmdline_param() {
|
||||
[[ -n "$f" ]] && grep -oE 'drm\.edid_firmware=[^ "]*steamify-[^ "]*' "$f" 2>/dev/null | head -n 1
|
||||
}
|
||||
|
||||
hdmi_available() {
|
||||
# Stays available while on, so it can be turned off after the pin is gone.
|
||||
detect_valve_fremont || return 1
|
||||
hdmi_status || { pinned_kernel_installed && [[ -n "$(hdmi_connectors)" && -n "$(hdmi_boot_file)" ]]; }
|
||||
hdmi_available() { detect_valve_fremont && hdmi_status; }
|
||||
|
||||
hdmi_status() {
|
||||
# On while anything of it is left: saved displays, the hotplug script,
|
||||
# an EDID file, or the kernel parameter of versions before 2.1.0.
|
||||
[[ -s "$HDMI_MAP" || -f "$HDMI_UNIT" || -n "$(hdmi_cmdline_param)" ]] ||
|
||||
compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null
|
||||
}
|
||||
|
||||
hdmi_status() { [[ -n "$(hdmi_cmdline_param)" ]] && compgen -G "$HDMI_FW_DIR/steamify-*.bin" >/dev/null; }
|
||||
|
||||
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 == '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.
|
||||
hdmi_reset_live() {
|
||||
# Puts a connected display back on its own EDID right away. debugfs is
|
||||
# root-only, so the glob runs under sudo; the kernel takes exactly
|
||||
# "reset", without a newline.
|
||||
local d
|
||||
d="$(sudo sh -c 'for d in /sys/kernel/debug/dri/*/"$1"; do [ -e "$d/edid_override" ] && { echo "$d"; break; }; done' _ "${1#card*-}")"
|
||||
[[ -n "$d" ]] && echo "$d"
|
||||
[[ -n "$d" ]] || return 1
|
||||
printf reset | sudo tee "$d/edid_override" >/dev/null &&
|
||||
echo 1 | sudo tee "$d/trigger_hotplug" >/dev/null
|
||||
}
|
||||
|
||||
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_boot_param() {
|
||||
# hdmi_boot_param <value|"">: set or remove our drm.edid_firmware= in
|
||||
# the boot loader's command line, then rebuild the initramfs and entries.
|
||||
local f value="$1"
|
||||
f="$(hdmi_boot_file)" || return 1
|
||||
[[ -n "$f" ]] || { err "No supported boot loader (Limine, systemd-boot with sdboot-manage, GRUB) found."; return 1; }
|
||||
hdmi_remove_boot_param() {
|
||||
# Removes the drm.edid_firmware= older versions put on the kernel command
|
||||
# line, then rebuilds the initramfs and boot entries.
|
||||
local f
|
||||
[[ -n "$(hdmi_cmdline_param)" ]] || { sudo rm -f "$HDMI_INITRAMFS_CONF"; return 0; }
|
||||
f="$(hdmi_boot_file)"
|
||||
backup_file "$f"
|
||||
sudo sed -i -E 's/drm\.edid_firmware=[^ "]*steamify-[^ "]* ?//' "$f"
|
||||
if [[ -n "$value" ]]; then
|
||||
case "$f" in
|
||||
# | as delimiter: the value holds a path.
|
||||
/etc/default/limine) sudo sed -i -E "0,\|^KERNEL_CMDLINE\[default\]\+?=\"|s||&$value |" "$f" ;;
|
||||
/etc/sdboot-manage.conf) sudo sed -i -E "0,\|^LINUX_OPTIONS=\"|s||&$value |" "$f" ;;
|
||||
/etc/default/grub) sudo sed -i -E "0,\|^GRUB_CMDLINE_LINUX_DEFAULT=\"|s||&$value |" "$f" ;;
|
||||
esac
|
||||
grep -qF "$value" "$f" || { err "Couldn't add $value to $f."; return 1; }
|
||||
fi
|
||||
sudo rm -f "$HDMI_INITRAMFS_CONF"
|
||||
info "Rebuilding the initramfs and boot entries..."
|
||||
case "$f" in
|
||||
/etc/default/limine) sudo limine-mkinitcpio ;;
|
||||
@@ -347,213 +77,27 @@ hdmi_boot_param() {
|
||||
esac
|
||||
}
|
||||
|
||||
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_forget_all() {
|
||||
# Every saved display and the hotplug script that loaded their EDIDs.
|
||||
local conn
|
||||
sudo rm -f "$HDMI_FW_DIR"/steamify-*.bin
|
||||
sudo systemctl disable "$HDMI_UNIT_NAME" >/dev/null 2>&1
|
||||
sudo rm -f "$HDMI_UDEV_RULE" "$HDMI_UNIT" "$HDMI_HOTPLUG" "$HDMI_MAP"
|
||||
sudo rm -rf "$HDMI_RUN"
|
||||
sudo systemctl daemon-reload
|
||||
sudo udevadm control --reload
|
||||
for conn in $(hdmi_connectors); do hdmi_reset_live "$conn" 2>/dev/null; done
|
||||
return 0
|
||||
}
|
||||
|
||||
hdmi_enable() {
|
||||
local -a HDMI_RESULTS=()
|
||||
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
|
||||
if hdmi_status; then
|
||||
local conn stale=false
|
||||
for conn in $(hdmi_connectors); do
|
||||
[[ -f "$HDMI_FW_DIR/steamify-${conn#card*-}.bin" ]] || continue
|
||||
[[ "$(state_get hdmi "${conn#card*-}")" == "$(hdmi_edid_tool id "/sys/class/drm/$conn/edid")" ]] || stale=true
|
||||
done
|
||||
if [[ "$stale" == false ]]; then
|
||||
ok "Already set up for the connected display(s). Untick and tick it again to re-test."
|
||||
return 0
|
||||
fi
|
||||
warn "Set up for another display; setting up the one connected now."
|
||||
hdmi_disable || 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 param="" files=() rc=0 res
|
||||
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%%|*}"
|
||||
sudo install -Dm644 "$tmp/$out.final" "$HDMI_FW_DIR/steamify-$out.bin" || { rc=1; continue; }
|
||||
state_set hdmi "$out" "$(hdmi_edid_tool id "$tmp/$out.orig")"
|
||||
state_set hdmi "$out-mode" "${res#*|}"
|
||||
param+="${param:+,}$out:edid/steamify-$out.bin"
|
||||
files+=("$HDMI_FW_DIR/steamify-$out.bin")
|
||||
done
|
||||
rm -rf "$tmp" "$HDMI_CACHE"
|
||||
[[ -n "$param" ]] || return $rc
|
||||
|
||||
# amdgpu loads from the initramfs (the kms hook), before the root
|
||||
# filesystem: the EDID file has to be in there too.
|
||||
sudo mkdir -p "$(dirname "$HDMI_INITRAMFS_CONF")"
|
||||
printf '# Written by steamify: EDID override for HDMI refresh boost.\nFILES+=(%s)\n' "${files[*]}" |
|
||||
sudo tee "$HDMI_INITRAMFS_CONF" >/dev/null
|
||||
if ! hdmi_boot_param "drm.edid_firmware=$param"; then
|
||||
# Nothing half-done left behind; the live EDID lasts until a restart.
|
||||
sudo rm -f "$HDMI_INITRAMFS_CONF" "${files[@]}"
|
||||
for conn in $(hdmi_connectors); do hdmi_override_live "$conn" reset 2>/dev/null; done
|
||||
state_clear hdmi
|
||||
err "Making it permanent failed; the display is back on its own EDID."
|
||||
return 1
|
||||
fi
|
||||
ok "HDMI refresh boost is set up; it's active now and after every restart."
|
||||
return $rc
|
||||
err "HDMI refresh boost is no longer offered: current kernels run HDMI displays at their full rate."
|
||||
return 1
|
||||
}
|
||||
|
||||
hdmi_disable() {
|
||||
local f conn
|
||||
# Drop-in and files first: the rebuild below leaves them out.
|
||||
sudo rm -f "$HDMI_INITRAMFS_CONF"
|
||||
for f in "$HDMI_FW_DIR"/steamify-*.bin; do [[ -e "$f" ]] && sudo rm -f "$f"; done
|
||||
hdmi_boot_param "" || return 1
|
||||
for conn in $(hdmi_connectors); do hdmi_override_live "$conn" reset 2>/dev/null; done
|
||||
hdmi_forget_all
|
||||
hdmi_remove_boot_param || return 1
|
||||
state_clear hdmi
|
||||
ok "HDMI refresh boost removed; the display uses its own EDID again."
|
||||
ok "HDMI refresh boost removed; displays use their own EDID again."
|
||||
}
|
||||
+86
-18
@@ -5,18 +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 hdmi 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 [hdmi]=machine [bios]=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. HDMI refresh boost needs someone at the
|
||||
# screen to confirm each step.
|
||||
NO_PRESELECT=(boot cec hdmi)
|
||||
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"
|
||||
@@ -25,9 +37,11 @@ declare -A LABEL=(
|
||||
[glyphs]="Install Steam Deck/Machine icons: Deck button icons in gaming mode"
|
||||
[single]="Single user mode: no password, lock screen or log out (SDDM)"
|
||||
[launcher]="Steamify shortcut: the app on the desktop, Steamify Terminal in the launcher"
|
||||
[vram]="VRAM booster: the game in front keeps its VRAM, background apps make room"
|
||||
[cec]="HDMI-CEC: use Steam with the TV remote, TV on/off with the PC (experimental)"
|
||||
[machine]="Steam Machine support: LED bar driver, hardware settings in Steam"
|
||||
[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"
|
||||
)
|
||||
@@ -35,7 +49,9 @@ 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
|
||||
@@ -56,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
|
||||
}
|
||||
|
||||
@@ -72,15 +130,28 @@ detect_components() {
|
||||
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
|
||||
@@ -96,14 +167,11 @@ 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; WANTED[bios]=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
|
||||
}
|
||||
|
||||
@@ -131,9 +199,9 @@ show_menu() {
|
||||
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]}"
|
||||
printf " %-3s %b %-6s └ %b\n" "$i" "$now" "$want" "$(menu_label "$c")"
|
||||
else
|
||||
printf " %-3s %b %-6s %s\n" "$i" "$now" "$want" "${LABEL[$c]}"
|
||||
printf " %-3s %b %-6s %b\n" "$i" "$now" "$want" "$(menu_label "$c")"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
@@ -266,7 +334,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
|
||||
@@ -281,13 +349,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[@]}")
|
||||
}
|
||||
+18
-9
@@ -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 were for the pinned kernel only (newer
|
||||
# ones read the whole EDID), also those of displays not connected now.
|
||||
hdmi_status && hdmi_forget_all
|
||||
remove_kernel_pin
|
||||
}
|
||||
|
||||
install_kernel_headers() {
|
||||
# DKMS can only build leds-valve against kernels whose headers are
|
||||
@@ -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,7 +355,8 @@ 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
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
# "VRAM booster" menu item (vram), ticked by default, like SteamOS 3.9's
|
||||
# dGPU VRAM management: the game in front keeps its VRAM, background apps
|
||||
# are evicted first, so a game that needs most of it doesn't spill into
|
||||
# system RAM and stutter. The kernel side is the dmem cgroup controller
|
||||
# (7.2; amdgpu and xe register their VRAM, NVIDIA's driver doesn't), so it's
|
||||
# only offered when the kernel lists a VRAM region of at least 2 GB. CachyOS packages the userspace
|
||||
# side: dmemcg-booster (a system service that enables the controller and
|
||||
# sets the limits, plus a user service) and plasma-foreground-booster, which
|
||||
# tells it which window is in front on the desktop. The latter only starts
|
||||
# with "autostart" set in kcgroupsrc.
|
||||
# Sourced by steamify.sh; not meant to be run on its own.
|
||||
|
||||
VRAM_PKGS=(dmemcg-booster plasma-foreground-booster)
|
||||
VRAM_MIN_BYTES=$((2 * 1024 * 1024 * 1024))
|
||||
|
||||
vram_supported() {
|
||||
# WIZARD_VRAM_FAKE_NVIDIA=1 (tests): as with an NVIDIA card.
|
||||
[[ -n "${WIZARD_VRAM_FAKE_NVIDIA:-}" ]] && return 1
|
||||
# Only for a GPU with its own VRAM: an integrated GPU registers a small
|
||||
# carve-out too, but mostly uses system RAM, so there's little to boost.
|
||||
awk -v min="$VRAM_MIN_BYTES" '$1 ~ /\/vram$/ && $2 >= min { found = 1 } END { exit !found }' \
|
||||
/sys/fs/cgroup/dmem.capacity 2>/dev/null
|
||||
}
|
||||
|
||||
vram_nvidia() {
|
||||
# An NVIDIA display controller (PCI vendor 10de, class 03xxxx).
|
||||
local d
|
||||
[[ -n "${WIZARD_VRAM_FAKE_NVIDIA:-}" ]] && return 0
|
||||
for d in /sys/bus/pci/devices/*; do
|
||||
[[ "$(cat "$d/vendor" 2>/dev/null)" == 0x10de && "$(cat "$d/class" 2>/dev/null)" == 0x03* ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Shown where it works, and greyed out with an NVIDIA card, whose driver
|
||||
# doesn't register its VRAM with the kernel (yet): so it's clear why it
|
||||
# can't be turned on. Hidden elsewhere (integrated graphics, older kernel).
|
||||
vram_available() { vram_supported || vram_status || vram_nvidia; }
|
||||
vram_selectable() { vram_supported || vram_status; }
|
||||
|
||||
vram_status() {
|
||||
pacman -Q "${VRAM_PKGS[@]}" >/dev/null 2>&1 &&
|
||||
systemctl is-enabled -q dmemcg-booster-system.service 2>/dev/null &&
|
||||
systemctl --user is-enabled -q dmemcg-booster-user.service 2>/dev/null
|
||||
}
|
||||
|
||||
vram_enable() {
|
||||
local p missing=()
|
||||
info "Installing the VRAM booster (the game in front keeps its VRAM)..."
|
||||
for p in "${VRAM_PKGS[@]}"; do pacman -Q "$p" >/dev/null 2>&1 || missing+=("$p"); done
|
||||
if ((${#missing[@]})); then
|
||||
sudo pacman -S --needed --noconfirm "${missing[@]}" ||
|
||||
{ err "Installing ${missing[*]} failed."; return 1; }
|
||||
state_set vram installed_pkgs "${missing[*]}"
|
||||
fi
|
||||
sudo systemctl enable --now dmemcg-booster-system.service >/dev/null 2>&1 ||
|
||||
{ err "Starting dmemcg-booster failed."; return 1; }
|
||||
systemctl --user enable --now dmemcg-booster-user.service >/dev/null 2>&1 ||
|
||||
{ err "Starting dmemcg-booster for $USER failed."; return 1; }
|
||||
kset vram kcgroupsrc "Foreground Booster" autostart true
|
||||
# Started by the Plasma session from now on; start it in this one too.
|
||||
[[ "${XDG_CURRENT_DESKTOP:-}" == KDE ]] &&
|
||||
systemctl --user start plasma-foreground-booster.service >/dev/null 2>&1
|
||||
ok "VRAM booster on: the game in front keeps its VRAM."
|
||||
}
|
||||
|
||||
vram_disable() {
|
||||
local pkgs
|
||||
systemctl --user disable --now plasma-foreground-booster.service dmemcg-booster-user.service >/dev/null 2>&1
|
||||
sudo systemctl disable --now dmemcg-booster-system.service >/dev/null 2>&1
|
||||
krevert vram
|
||||
pkgs="$(state_get vram installed_pkgs)"
|
||||
# shellcheck disable=SC2086 # a list of package names
|
||||
[[ -n "$pkgs" ]] && sudo pacman -Rns --noconfirm $pkgs >/dev/null 2>&1
|
||||
state_clear vram
|
||||
ok "VRAM booster off."
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# Patches and extra sources
|
||||
|
||||
Files Steamify builds or applies, kept out of the shell code so they can be
|
||||
read and reviewed on their own. `lib/*.sh` reads them with `patch_file
|
||||
<name>`; the single-file build (`.github/tools/bundle.sh`) embeds them.
|
||||
|
||||
| File | Used by | What |
|
||||
|---|---|---|
|
||||
| `steamify-fremont-poweroff.c` | `lib/fremont-poweroff.sh` (Steam Machine support) | Kernel module (DKMS): clears GPIO 18's S4/S5 wake bit before power-off, so the Steam Machine stays off with recent kernels |
|
||||
| `cros-ec-cec-single-port.patch` | `lib/cec.sh` (HDMI-CEC) | Makes Valve's Steam Machine CEC driver find amdgpu's HDMI port |
|
||||
@@ -0,0 +1,21 @@
|
||||
Steam Machine CEC: find amdgpu's HDMI port with a single CEC port.
|
||||
|
||||
amdgpu registers its HDMI notifier without a port name, which only matches
|
||||
the driver's named lookup ("Port C") if the CEC driver loaded first; amdgpu
|
||||
loads from the initramfs, so it never does and /dev/cec0 stays at f.f.f.f.
|
||||
With a single CEC port, look the notifier up by device alone.
|
||||
|
||||
Applies to Valve's drivers/media/cec/platform/cros-ec/cros-ec-cec.c
|
||||
(evlaV/linux-integration 10c8c880, see CEC_DRIVER_URL in lib/cec.sh).
|
||||
|
||||
--- a/cros-ec-cec.c
|
||||
+++ b/cros-ec-cec.c
|
||||
@@ -485,7 +485,7 @@
|
||||
goto out_probe_adapter;
|
||||
}
|
||||
|
||||
- port->notify = cec_notifier_cec_adap_register(hdmi_dev, conns[port_num],
|
||||
+ port->notify = cec_notifier_cec_adap_register(hdmi_dev, conns[1] ? conns[port_num] : NULL,
|
||||
port->adap);
|
||||
if (!port->notify) {
|
||||
ret = -ENOMEM;
|
||||
@@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* Steam Machine (Fremont): power off instead of booting up again.
|
||||
*
|
||||
* Since "pinctrl-amd: Don't clear S4 wake bits at probe" (Linux 7.2, also
|
||||
* backported to 6.x, 7.0 and 7.1 updates) the S4/S5 wake bit the firmware
|
||||
* leaves set on GPIO pin 18 (_SB.PCI0.GPP6) stays set, and the machine starts again right after powering off.
|
||||
* Valve's kernel clears that bit at probe on Fremont (not for upstream);
|
||||
* this does the same right before power-off, for kernels without it.
|
||||
*/
|
||||
#include <linux/acpi.h>
|
||||
#include <linux/bits.h>
|
||||
#include <linux/dmi.h>
|
||||
#include <linux/io.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/platform_device.h>
|
||||
#include <linux/reboot.h>
|
||||
|
||||
#define FREMONT_GPP6_PIN 18
|
||||
#define WAKE_CNTRL_OFF_S4 15
|
||||
|
||||
static void __iomem *pin_reg;
|
||||
static struct sys_off_handler *handler;
|
||||
|
||||
static int fremont_poweroff_prepare(struct sys_off_data *data)
|
||||
{
|
||||
u32 v = readl(pin_reg);
|
||||
|
||||
if (v & BIT(WAKE_CNTRL_OFF_S4))
|
||||
writel(v & ~BIT(WAKE_CNTRL_OFF_S4), pin_reg);
|
||||
return NOTIFY_DONE;
|
||||
}
|
||||
|
||||
static int __init fremont_poweroff_init(void)
|
||||
{
|
||||
struct acpi_device *adev;
|
||||
struct device *dev;
|
||||
struct resource *res;
|
||||
u32 v;
|
||||
|
||||
if (!dmi_match(DMI_BOARD_NAME, "Fremont"))
|
||||
return -ENODEV;
|
||||
adev = acpi_dev_get_first_match_dev("AMDI0030", NULL, -1);
|
||||
if (!adev)
|
||||
return -ENODEV;
|
||||
dev = bus_find_device_by_acpi_dev(&platform_bus_type, adev);
|
||||
acpi_dev_put(adev);
|
||||
if (!dev)
|
||||
return -ENODEV;
|
||||
res = platform_get_resource(to_platform_device(dev), IORESOURCE_MEM, 0);
|
||||
put_device(dev);
|
||||
if (!res || resource_size(res) < (FREMONT_GPP6_PIN + 1) * 4)
|
||||
return -ENODEV;
|
||||
/* pinctrl-amd owns the region; only this one register is touched. */
|
||||
pin_reg = ioremap(res->start + FREMONT_GPP6_PIN * 4, 4);
|
||||
if (!pin_reg)
|
||||
return -ENOMEM;
|
||||
v = readl(pin_reg);
|
||||
pr_info("GPIO %d register 0x%08x, S4/S5 wake %s\n", FREMONT_GPP6_PIN, v,
|
||||
v & BIT(WAKE_CNTRL_OFF_S4) ? "set (cleared at power-off)" : "clear");
|
||||
handler = register_sys_off_handler(SYS_OFF_MODE_POWER_OFF_PREPARE,
|
||||
SYS_OFF_PRIO_DEFAULT, fremont_poweroff_prepare, NULL);
|
||||
if (IS_ERR(handler)) {
|
||||
iounmap(pin_reg);
|
||||
return PTR_ERR(handler);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void __exit fremont_poweroff_exit(void)
|
||||
{
|
||||
unregister_sys_off_handler(handler);
|
||||
iounmap(pin_reg);
|
||||
}
|
||||
|
||||
module_init(fremont_poweroff_init);
|
||||
module_exit(fremont_poweroff_exit);
|
||||
MODULE_DESCRIPTION("Steam Machine: clear GPIO 18's S4 wake bit before power-off");
|
||||
MODULE_LICENSE("GPL");
|
||||
MODULE_ALIAS("dmi:*:rnFremont:*");
|
||||
+6
-3
@@ -15,11 +15,11 @@
|
||||
set -uo pipefail
|
||||
|
||||
# Release version, see CHANGELOG.md.
|
||||
VERSION=2.0.3
|
||||
VERSION=2.4.1
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
for lib in common state packages login-manager single-user steam-desktop steam-machine hdmi-refresh cec boot-session vapor-theme steamos-extras bios desktop-shortcut wizard-shortcut menu backend; 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
|
||||
@@ -139,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; }
|
||||
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
// State and logic of the app: menu -> review -> password -> applying -> done,
|
||||
// and the BIOS update (check -> warning 1 -> warning 2 -> flash). The
|
||||
// screens only show this and call its functions.
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: app
|
||||
required property var backend
|
||||
required property var gamepad
|
||||
|
||||
property string screen: "menu" // menu review password applying done bios1 bios2
|
||||
property double remoteAt: 0
|
||||
property var want: ({})
|
||||
property string boot: "gamescope"
|
||||
property int sel: 0 // row index; rows.length = the Apply button
|
||||
property bool reapply: false
|
||||
property var plan: [] // [{id, action, isNew}]
|
||||
property var steps: ({}) // id -> "wait"|"run"|"ok"|"fail"
|
||||
property var failed: []
|
||||
property bool restartNeeded: false
|
||||
property string runError: ""
|
||||
property bool wrongPassword: false
|
||||
property bool typing: false // a text field has the keys
|
||||
property int doneCount: 0
|
||||
property string pending: "apply" // what the password is for: apply | bios
|
||||
property string sessionPassword: "" // kept between the BIOS steps only
|
||||
property var biosInfo: ({})
|
||||
property bool biosRun: false
|
||||
property bool biosChecking: false // the invisible check before the warnings
|
||||
property int biosFocus: 0 // warning 1: 0 = Cancel, 1 = continue
|
||||
property real holdProgress: 0 // warning 2: hold A/OK for 5 s
|
||||
property bool holding: false
|
||||
readonly property alias log: logModel
|
||||
|
||||
readonly property var status: backend.status || ({})
|
||||
readonly property var items: status.items || []
|
||||
readonly property var rows: items.filter(function (i) { return !i.parent || want[i.parent]; })
|
||||
readonly property var bios: status.bios || null
|
||||
|
||||
signal passwordRequested()
|
||||
signal passwordRejected()
|
||||
signal passwordSubmitRequested() // A/OK on the password screen
|
||||
|
||||
ListModel { id: logModel }
|
||||
|
||||
function nowOn(id) {
|
||||
for (var i = 0; i < items.length; i++) if (items[i].id === id) return items[i].on;
|
||||
return false;
|
||||
}
|
||||
function label(item) { return Texts.label(item.id, item.label); }
|
||||
function biosHint() {
|
||||
if (!bios) return "";
|
||||
if (!bios.newest) return "Now " + bios.current + ", newest unknown (offline?)";
|
||||
if (bios.current === bios.newest) return bios.current + " is up to date";
|
||||
return "Now " + bios.current + ", newest " + bios.newest + " · at your own risk";
|
||||
}
|
||||
function syncFromStatus() {
|
||||
reapply = false;
|
||||
var w = {};
|
||||
for (var i = 0; i < items.length; i++) w[items[i].id] = items[i].kind === "action" ? false : items[i].wanted;
|
||||
want = w;
|
||||
boot = nowOn("boot") ? "desktop" : "gamescope";
|
||||
if (sel > rows.length) sel = 0;
|
||||
}
|
||||
Connections { target: backend; function onStatusChanged() { if (app.screen === "menu" || app.screen === "done") app.syncFromStatus(); } }
|
||||
|
||||
// --- Menu ---
|
||||
function toggle(id) {
|
||||
var cur = items.find(function (i) { return i.id === id; });
|
||||
if (cur && cur.kind === "toggle" && cur.selectable === false) return;
|
||||
var w = Object.assign({}, want);
|
||||
w[id] = !w[id];
|
||||
// The same rules as the terminal menu (toggle_component).
|
||||
if (id === "gaming" && !w.gaming) w.single = false;
|
||||
if (id === "single" && w.single) w.gaming = true;
|
||||
if (id === "machine") w.poweroff = w.machine;
|
||||
if (id === "poweroff" && w.poweroff) w.machine = true;
|
||||
if (id === "machine" && !w.machine) w.kpin = false;
|
||||
if (id === "kpin" && w.kpin) w.machine = true;
|
||||
want = w;
|
||||
}
|
||||
function computePlan() {
|
||||
var p = [];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var it = items[i];
|
||||
if (it.kind !== "toggle") continue;
|
||||
if (it.parent && !want[it.parent]) { if (it.on) p.push({ id: it.id, action: "off" }); continue; }
|
||||
if (want[it.id] && !it.on) p.push({ id: it.id, action: "on", isNew: !!it["new"] });
|
||||
else if (!want[it.id] && it.on) p.push({ id: it.id, action: "off" });
|
||||
else if (want[it.id] && (reapply || it.update)) p.push({ id: it.id, action: reapply ? "again" : "update" });
|
||||
}
|
||||
var bootNow = nowOn("boot") ? "desktop" : "gamescope";
|
||||
if (want.gaming && boot !== bootNow) p.push({ id: "boot", action: boot === "desktop" ? "desktop" : "gaming" });
|
||||
return p;
|
||||
}
|
||||
function goReview(again) { reapply = again; plan = computePlan(); screen = "review"; }
|
||||
function wantedIds() {
|
||||
var ids = [];
|
||||
for (var i = 0; i < items.length; i++)
|
||||
if (items[i].kind === "toggle" && want[items[i].id] && (!items[i].parent || want[items[i].parent])) ids.push(items[i].id);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// --- Password and apply ---
|
||||
function resetRun() { steps = {}; failed = []; restartNeeded = false; runError = ""; doneCount = 0; logModel.clear(); }
|
||||
function startApply(password) {
|
||||
resetRun();
|
||||
var s = {};
|
||||
for (var i = 0; i < plan.length; i++) s[plan[i].id] = "wait";
|
||||
steps = s;
|
||||
screen = "applying";
|
||||
backend.apply(wantedIds(), want.gaming ? boot : "", reapply, password || "");
|
||||
}
|
||||
function askPassword(forWhat) {
|
||||
pending = forWhat;
|
||||
if (!backend.needsPassword()) return false;
|
||||
wrongPassword = false; screen = "password"; passwordRequested();
|
||||
return true;
|
||||
}
|
||||
function onApplyPressed() {
|
||||
if (plan.length === 0) return;
|
||||
if (!askPassword("apply")) startApply("");
|
||||
}
|
||||
function submitPassword(p) {
|
||||
if (!backend.checkPassword(p)) { wrongPassword = true; passwordRejected(); return; }
|
||||
if (pending === "bios") { sessionPassword = p; biosCheck(); }
|
||||
else startApply(p);
|
||||
}
|
||||
function passwordBack() { screen = pending === "bios" ? "menu" : "review"; }
|
||||
|
||||
// --- BIOS ---
|
||||
function startBios() {
|
||||
if (!bios || !bios.selectable) return;
|
||||
biosInfo = {}; sessionPassword = "";
|
||||
if (!askPassword("bios")) biosCheck();
|
||||
}
|
||||
function biosCheck() {
|
||||
// Invisible: stay on the menu (the BIOS row says "Checking…"),
|
||||
// then the first warning.
|
||||
failed = []; runError = ""; logModel.clear();
|
||||
plan = [{ id: "bios", action: "check" }];
|
||||
biosRun = true; biosChecking = true; screen = "menu";
|
||||
backend.biosPrepare(sessionPassword);
|
||||
}
|
||||
function biosCancel() { sessionPassword = ""; holdProgress = 0; holding = false; biosRun = false; syncFromStatus(); screen = "menu"; }
|
||||
function biosContinue() { holdProgress = 0; screen = "bios2"; }
|
||||
function biosFlash() {
|
||||
holdProgress = 0; holding = false;
|
||||
resetRun();
|
||||
plan = [{ id: "bios", action: "flash" }];
|
||||
steps = { bios: "wait" };
|
||||
biosRun = true; screen = "applying";
|
||||
backend.biosFlash(sessionPassword);
|
||||
}
|
||||
Timer {
|
||||
interval: 50; repeat: true; running: app.screen === "bios2" && app.holding
|
||||
onTriggered: { app.holdProgress = Math.min(1, app.holdProgress + 0.01); if (app.holdProgress >= 1) app.biosFlash(); }
|
||||
}
|
||||
|
||||
function backToMenu() { biosRun = false; syncFromStatus(); screen = "menu"; }
|
||||
|
||||
// --- Backend events ---
|
||||
function errorText(e) {
|
||||
return e === "wrong-password" ? "The password didn't work." : (e === "start" ? "Steamify couldn't start the changes." : (e ? "sudo isn't available." : ""));
|
||||
}
|
||||
Connections {
|
||||
target: app.backend
|
||||
function onEvent(ev) {
|
||||
var s = Object.assign({}, app.steps);
|
||||
if (ev.event === "start") { s[ev.id] = "run"; app.steps = s; }
|
||||
else if (ev.event === "done") { s[ev.id] = ev.ok ? "ok" : "fail"; app.steps = s; app.doneCount++; }
|
||||
else if (ev.event === "log") { logModel.append({ line: ev.line }); if (logModel.count > 400) logModel.remove(0); }
|
||||
else if (ev.event === "bios-ready") { app.biosChecking = false; app.biosInfo = ev; app.biosFocus = 0; app.holdProgress = 0; app.screen = "bios1"; }
|
||||
else if (ev.event === "finished" && app.biosChecking) {
|
||||
// The check found nothing to do or a problem: say so.
|
||||
app.biosChecking = false; app.sessionPassword = "";
|
||||
app.failed = ev.failed || []; app.restartNeeded = false;
|
||||
app.runError = ev.nothing && !ev.error ? "The BIOS is already the newest version." : app.errorText(ev.error);
|
||||
app.screen = "done";
|
||||
}
|
||||
else if (ev.event === "finished") {
|
||||
// A successful BIOS check ends with bios-ready instead, keeping
|
||||
// the password for the flash.
|
||||
app.sessionPassword = "";
|
||||
app.failed = ev.failed || []; app.restartNeeded = !!ev.restart;
|
||||
app.runError = app.errorText(ev.error);
|
||||
app.screen = "done";
|
||||
}
|
||||
}
|
||||
function onRemotePressed() { app.remoteAt = Date.now(); Input.type = "remote"; }
|
||||
}
|
||||
|
||||
// --- Input: one set of actions for controller, keyboard and remote ---
|
||||
function act(a) {
|
||||
if (screen === "menu") {
|
||||
if (biosChecking) return;
|
||||
if (a === "up") sel = Math.max(0, sel - 1);
|
||||
else if (a === "down") sel = Math.min(rows.length, sel + 1);
|
||||
else if (a === "accept") {
|
||||
if (sel === rows.length) { goReview(false); return; }
|
||||
var r = rows[sel];
|
||||
if (r.kind === "action" && r.id === "bios") startBios();
|
||||
else if (r.kind === "choice") boot = boot === "gamescope" ? "desktop" : "gamescope";
|
||||
else if (r.kind === "toggle") toggle(r.id);
|
||||
}
|
||||
else if ((a === "left" || a === "right") && sel < rows.length && rows[sel].kind === "choice")
|
||||
boot = a === "left" ? "gamescope" : "desktop";
|
||||
else if (a === "apply") goReview(false);
|
||||
else if (a === "reapply") goReview(true);
|
||||
else if (a === "back") Qt.quit();
|
||||
} else if (screen === "review") {
|
||||
if (a === "accept" || a === "apply") onApplyPressed();
|
||||
else if (a === "back") { reapply = false; screen = "menu"; }
|
||||
} else if (screen === "password") {
|
||||
if (a === "accept" || a === "apply") passwordSubmitRequested();
|
||||
else if (a === "back") passwordBack();
|
||||
} else if (screen === "bios1") {
|
||||
if (a === "left") biosFocus = 0;
|
||||
else if (a === "right") biosFocus = 1;
|
||||
else if (a === "accept") { if (biosFocus === 1) biosContinue(); else biosCancel(); }
|
||||
else if (a === "back") biosCancel();
|
||||
} else if (screen === "bios2") {
|
||||
if (a === "back") biosCancel();
|
||||
else if (a === "hold") holding = true;
|
||||
else if (a === "release") { holding = false; if (holdProgress < 1) holdProgress = 0; }
|
||||
} else if (screen === "done") {
|
||||
if (a === "accept" && restartNeeded) backend.restart();
|
||||
else if (a === "back" || a === "accept") backToMenu();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: app.gamepad
|
||||
function onButton(b) {
|
||||
if (b === "a_up") { if (app.screen === "bios2") app.act("release"); return; }
|
||||
if (b.slice(-3) === "_up") return;
|
||||
Input.type = "controller";
|
||||
if (b === "a" && app.screen === "bios2") { app.act("hold"); return; }
|
||||
var map = { a: "accept", b: "back", x: "apply", y: "reapply", start: "apply", up: "up", down: "down", left: "left", right: "right" };
|
||||
if (map[b]) app.act(map[b]);
|
||||
}
|
||||
}
|
||||
function keyPressed(e) {
|
||||
// A text field handles its own keys, except Escape and Enter.
|
||||
if (typing && e.key !== Qt.Key_Escape && e.key !== Qt.Key_Return && e.key !== Qt.Key_Enter) return;
|
||||
var fromRemote = Date.now() - remoteAt < 400;
|
||||
if (screen === "bios2" && Input.type !== "keyboard" && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) {
|
||||
if (!e.isAutoRepeat) act("hold"); e.accepted = true; return;
|
||||
}
|
||||
if (!fromRemote) Input.type = "keyboard";
|
||||
var k = e.key;
|
||||
if (k === Qt.Key_Up) act("up");
|
||||
else if (k === Qt.Key_Down) act("down");
|
||||
else if (k === Qt.Key_Left) act("left");
|
||||
else if (k === Qt.Key_Right) act("right");
|
||||
else if (k === Qt.Key_Space) act("accept");
|
||||
else if (k === Qt.Key_Return || k === Qt.Key_Enter) act(Input.type === "remote" || screen !== "menu" ? "accept" : "apply");
|
||||
else if (k === Qt.Key_Escape || k === Qt.Key_Back || k === Qt.Key_Backspace) { if (!e.isAutoRepeat) act("back"); }
|
||||
else if (k === Qt.Key_R) act("reapply");
|
||||
else return;
|
||||
e.accepted = true;
|
||||
}
|
||||
function keyReleased(e) {
|
||||
if (screen === "bios2" && !e.isAutoRepeat && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) act("release");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Progress: every step of the plan on the left, the log on the right.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Item {
|
||||
required property AppState app
|
||||
readonly property bool checking: app.biosRun && app.plan.length > 0 && app.plan[0].action === "check"
|
||||
Column {
|
||||
x: 40; y: 32; width: 560; spacing: 18
|
||||
Text { text: app.biosRun ? (checking ? "Checking the BIOS update" : "Installing the BIOS update") : "Applying your changes"
|
||||
color: Theme.text; font.family: Theme.display; font.pixelSize: 34; font.weight: Font.DemiBold }
|
||||
Rectangle { width: 560; height: 10; radius: 5; color: Theme.line
|
||||
Rectangle { height: 10; radius: 5; color: Theme.accent; width: app.plan.length ? parent.width * Math.min(1, (app.doneCount + 0.3) / app.plan.length) : 0
|
||||
Behavior on width { NumberAnimation { duration: 300 } } } }
|
||||
Row { width: 560
|
||||
Text { text: "Step " + Math.min(app.plan.length, app.doneCount + 1) + " of " + app.plan.length; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14; width: 280 }
|
||||
Text { text: "Don't turn off the PC"; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14; width: 280; horizontalAlignment: Text.AlignRight } }
|
||||
ListView {
|
||||
width: 560; clip: true; spacing: 8
|
||||
height: parent.parent.height - 32 - 156 - 32
|
||||
model: app.plan
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
// Follow the progress: the running step stays in view.
|
||||
readonly property int running: app.plan.findIndex(function (p) { return app.steps[p.id] === "run"; })
|
||||
onRunningChanged: if (running >= 0) positionViewAtIndex(running, ListView.Contain)
|
||||
delegate: Rectangle {
|
||||
id: step
|
||||
required property var modelData
|
||||
readonly property string st: app.steps[modelData.id] || "wait"
|
||||
width: 560; height: 54; radius: 12; color: st === "run" ? Theme.cardSel : Theme.card
|
||||
border.width: st === "run" ? 2 : 0; border.color: Theme.accent
|
||||
Row { anchors.fill: parent; anchors.leftMargin: 18; anchors.rightMargin: 18; spacing: 14
|
||||
Rectangle { width: 22; height: 22; radius: 11; anchors.verticalCenter: parent.verticalCenter
|
||||
color: step.st === "ok" ? Theme.goodBg : (step.st === "fail" ? Theme.badBg : "transparent")
|
||||
border.width: step.st === "wait" || step.st === "run" ? 2 : 0; border.color: step.st === "run" ? Theme.accent : "#343f50"
|
||||
Text { anchors.centerIn: parent; text: step.st === "ok" ? "✓" : (step.st === "fail" ? "!" : ""); color: step.st === "ok" ? Theme.good : Theme.bad
|
||||
font.pixelSize: 13; font.weight: Font.Bold }
|
||||
RotationAnimator on rotation { running: step.st === "run"; from: 0; to: 360; duration: 1000; loops: Animation.Infinite } }
|
||||
Row { width: 380; anchors.verticalCenter: parent.verticalCenter; spacing: 10
|
||||
Text { text: (Texts.actions[step.modelData.action] || [])[3] + Texts.label(step.modelData.id)
|
||||
color: step.st === "wait" ? Theme.faint : Theme.textHi; font.family: Theme.body; font.pixelSize: 16; font.weight: Font.DemiBold
|
||||
anchors.verticalCenter: parent.verticalCenter; elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, parent.width - (badge.visible ? badge.width + parent.spacing : 0)) }
|
||||
Badge { id: badge; update: step.modelData.action === "update"; isNew: !!step.modelData.isNew; anchors.verticalCenter: parent.verticalCenter } }
|
||||
Text { text: ({ wait: "Waiting", run: "Working…", ok: "Done", fail: "Problem" })[step.st]
|
||||
color: step.st === "ok" ? Theme.good : (step.st === "fail" ? Theme.bad : Theme.label); font.family: Theme.body; font.pixelSize: 13
|
||||
anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
x: 632; y: 32; width: parent.width - 672; height: parent.height - 64; spacing: 10
|
||||
Text { text: "DETAILS"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
|
||||
Rectangle { width: parent.width; height: parent.height - 30; radius: 16; color: "#0b0e13"; border.width: 1; border.color: "#1d2531"
|
||||
ListView { id: logView; anchors.fill: parent; anchors.margins: 18; clip: true; model: app.log
|
||||
onCountChanged: positionViewAtEnd()
|
||||
delegate: Text { required property string line; width: logView.width; text: line; color: "#aab5c4"; font.family: Theme.mono; font.pixelSize: 13; wrapMode: Text.WrapAnywhere } } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// "Update" (a newer version of it will be applied) or "New" (a default
|
||||
// option added since the last run), after an item's name.
|
||||
import QtQuick
|
||||
|
||||
Chip {
|
||||
property bool update: false
|
||||
property bool isNew: false
|
||||
visible: update || isNew
|
||||
height: 20
|
||||
text: update ? "Update" : "New"
|
||||
fg: update ? Theme.warn : Theme.good
|
||||
bgc: update ? Theme.warnBg : Theme.goodBg
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// The bar at the bottom of a screen, for its buttons.
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom; width: parent.width; height: 64; color: Theme.bar
|
||||
Rectangle { width: parent.width; height: 1; color: Theme.line }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// BIOS update, warning 2 (last chance): type UPDATE on a keyboard, or hold
|
||||
// A/OK for 5 seconds on a controller or remote.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Item {
|
||||
id: screen
|
||||
required property AppState app
|
||||
onVisibleChanged: if (visible) { typed.text = ""; if (Input.type === "keyboard") typed.forceActiveFocus(); }
|
||||
Binding { target: screen.app; property: "typing"; value: typed.activeFocus; when: screen.visible }
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent; width: 760; height: col.implicitHeight + 64; radius: 20; color: Theme.dangerBg; border.width: 2; border.color: Theme.dangerLine
|
||||
Column { id: col; anchors.fill: parent; anchors.margins: 32; spacing: 18
|
||||
Text { text: "Last chance: this flashes BIOS " + (app.biosInfo.newest || ""); color: "#ffd9d3"; font.family: Theme.display; font.pixelSize: 32; font.weight: Font.DemiBold }
|
||||
Text { width: parent.width; wrapMode: Text.WordWrap; color: Theme.soft; font.family: Theme.body; font.pixelSize: 16; lineHeight: 1.3
|
||||
text: "After this, keep the power on until the machine has fully started again. Don't touch it, even if the screen is black." }
|
||||
Column { visible: Input.type === "keyboard"; width: parent.width; spacing: 8
|
||||
Text { text: "Type UPDATE (in capitals) and press Enter to flash the BIOS:"; color: Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold }
|
||||
TextField { id: typed; width: 320; height: 50; color: Theme.textHi; font.family: Theme.mono; font.pixelSize: 20; leftPadding: 14
|
||||
placeholderText: "UPDATE"; placeholderTextColor: "#6b5a5a"
|
||||
background: Rectangle { radius: 10; color: Theme.bg; border.width: 2; border.color: typed.text === "UPDATE" ? Theme.bad : "#5a3a3a" }
|
||||
Keys.onReturnPressed: if (text === "UPDATE") app.biosFlash()
|
||||
Keys.onEnterPressed: if (text === "UPDATE") app.biosFlash()
|
||||
Keys.onEscapePressed: app.biosCancel() } }
|
||||
Column { visible: Input.type !== "keyboard"; width: parent.width; spacing: 10
|
||||
Text { text: "Hold " + Input.g.ok + " for 5 seconds to flash the BIOS. Let go to stop."; color: Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold }
|
||||
Rectangle { width: parent.width; height: 14; radius: 7; color: "#3a2224"
|
||||
Rectangle { height: 14; radius: 7; color: Theme.bad; width: parent.width * app.holdProgress } } }
|
||||
Row { spacing: 12
|
||||
Btn { k: Input.g.back; text: "Cancel"; onClicked: app.biosCancel() }
|
||||
Text { visible: app.bios && app.bios.dryRun; text: "Dry run: nothing will be flashed."; color: Theme.warn; font.family: Theme.body; font.pixelSize: 14
|
||||
anchors.verticalCenter: parent.verticalCenter } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// BIOS update, warning 1: the risks and what was checked; Cancel is
|
||||
// selected first.
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
required property AppState app
|
||||
Rectangle {
|
||||
x: 40; y: 28; width: parent.width - 80; height: parent.height - 28 - 64 - 24; radius: 16; color: Theme.dangerBg; border.width: 2; border.color: Theme.dangerLine
|
||||
Row { anchors.fill: parent; anchors.margins: 32; spacing: 40
|
||||
Column { width: 640; spacing: 16
|
||||
Row { spacing: 12
|
||||
Rectangle { width: 44; height: 44; radius: 12; color: "#3a1c1f"
|
||||
Text { anchors.centerIn: parent; text: "!"; color: Theme.bad; font.pixelSize: 26; font.weight: Font.Bold } }
|
||||
Text { text: "BIOS update – entirely at your own risk"; color: "#ffd9d3"; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold
|
||||
anchors.verticalCenter: parent.verticalCenter } }
|
||||
RiskList { width: 640 }
|
||||
}
|
||||
Rectangle { width: 440; height: 250; radius: 14; color: Theme.card
|
||||
Column { anchors.fill: parent; anchors.margins: 22; spacing: 12
|
||||
Repeater { model: [["Current BIOS", app.biosInfo.current || ""], ["New BIOS", app.biosInfo.newest || ""], ["Checksum", "OK (Valve's package)"],
|
||||
["Compatible", app.biosInfo.compatible === "yes" ? "Yes (checked by fwupd)" : "Not checked (dry run)"]]
|
||||
Row { required property var modelData; width: 396
|
||||
Text { text: parent.modelData[0]; color: Theme.faint; font.family: Theme.body; font.pixelSize: 15; width: 150 }
|
||||
Text { text: parent.modelData[1]; color: parent.modelData[1].indexOf("Not") === 0 ? Theme.warn : Theme.text; font.family: Theme.mono; font.pixelSize: 15
|
||||
width: 246; horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } } }
|
||||
} }
|
||||
}
|
||||
}
|
||||
Bar {
|
||||
Text { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; text: "Do you understand the risks and want to continue?"
|
||||
color: Theme.soft; font.family: Theme.body; font.pixelSize: 15 }
|
||||
Row { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 12
|
||||
Btn { k: app.biosFocus === 0 ? Input.g.ok : "◀"; text: "Cancel"; focusRing: app.biosFocus === 0; onClicked: app.biosCancel() }
|
||||
Btn { k: app.biosFocus === 1 ? Input.g.ok : "▶"; text: "I understand, continue"; focusRing: app.biosFocus === 1; color: Theme.danger
|
||||
onClicked: app.biosContinue() } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// A button with the input's button name in front.
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: btn
|
||||
property string k
|
||||
property string text
|
||||
property bool primary: false
|
||||
property bool focusRing: false
|
||||
signal clicked
|
||||
height: 48; radius: 12; width: row.implicitWidth + 36
|
||||
color: primary ? Theme.accent : Theme.button
|
||||
border.width: focusRing ? 2 : 0; border.color: Theme.textHi
|
||||
Row { id: row; anchors.centerIn: parent; spacing: 10
|
||||
Glyph { k: btn.k; visible: btn.k !== ""; dark: btn.primary; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: btn.text; color: btn.primary ? Theme.ink : Theme.text; font.family: Theme.body; font.pixelSize: 15; font.weight: Font.DemiBold
|
||||
anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: btn.clicked() }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// A small label in capitals.
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: chip
|
||||
property string text
|
||||
property color fg: Theme.good
|
||||
property color bgc: Theme.goodBg
|
||||
height: 24; radius: 6; width: ct.implicitWidth + 16; color: bgc
|
||||
Text { id: ct; anchors.centerIn: parent; text: chip.text.toUpperCase(); color: chip.fg
|
||||
font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.6 }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Right of the menu: what the selected item does, or a nudge to review
|
||||
// when the Apply button is selected.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Rectangle {
|
||||
id: detail
|
||||
required property AppState app
|
||||
readonly property var it: app.sel < app.rows.length ? app.rows[app.sel] : null
|
||||
readonly property var tx: it ? Texts.of(it) : ({})
|
||||
radius: 16; color: Theme.card
|
||||
// Long texts scroll instead of running out of the card; each item
|
||||
// starts at the top.
|
||||
onItChanged: flick.contentY = 0
|
||||
|
||||
Flickable {
|
||||
id: flick
|
||||
anchors.fill: parent; anchors.margins: 24; anchors.rightMargin: 12
|
||||
visible: detail.it !== null; clip: true
|
||||
contentHeight: col.implicitHeight; boundsBehavior: Flickable.StopAtBounds
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
Column {
|
||||
id: col; width: flick.width - 12; spacing: 14
|
||||
Row { spacing: 10
|
||||
Chip { readonly property var it: detail.it
|
||||
text: !it ? "" : it.kind === "choice" ? (app.boot === "desktop" ? "Desktop" : "Gaming") : it.kind === "action" ? "Opt-in" : (it.on ? "On" : "Off")
|
||||
fg: it && it.on ? Theme.good : Theme.label; bgc: it && it.on ? Theme.goodBg : Theme.line }
|
||||
Chip { visible: !!detail.tx.experimental; text: "Experimental"; fg: Theme.warn; bgc: Theme.warnBg }
|
||||
}
|
||||
Text { text: detail.it ? app.label(detail.it) : ""; color: Theme.text; font.family: Theme.display; font.pixelSize: 26; font.weight: Font.DemiBold
|
||||
wrapMode: Text.WordWrap; width: parent.width }
|
||||
Text { text: detail.tx.body || ""; color: Theme.soft; font.family: Theme.body; font.pixelSize: 15; lineHeight: 1.4; wrapMode: Text.WordWrap; width: parent.width }
|
||||
Text { text: "WHAT IT CHANGES"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8; topPadding: 4 }
|
||||
Repeater { model: detail.tx.changes || []
|
||||
Text { required property string modelData; text: "• " + modelData; color: Theme.soft; font.family: Theme.body; font.pixelSize: 14
|
||||
wrapMode: Text.WordWrap; width: col.width } }
|
||||
}
|
||||
}
|
||||
Column {
|
||||
anchors.centerIn: parent; spacing: 8; visible: detail.it === null
|
||||
Text { text: app.computePlan().length === 0 ? "Everything is the way you want it" : "Review what will change"; color: Theme.text
|
||||
font.family: Theme.display; font.pixelSize: 24; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
Text { text: "then apply it"; color: Theme.mute; font.family: Theme.body; font.pixelSize: 15; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// The result of a run: all done, a problem, or nothing changed.
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
required property AppState app
|
||||
readonly property bool problem: app.failed.length > 0 || app.runError !== ""
|
||||
readonly property string message: {
|
||||
if (app.runError) return app.runError;
|
||||
if (!app.biosRun) return app.plan.length + (app.plan.length === 1 ? " change" : " changes") + " applied." + (app.restartNeeded ? " Some take effect after a restart." : "");
|
||||
if (app.failed.length) return "The BIOS was not changed.";
|
||||
return (app.bios && app.bios.dryRun ? "Dry run: nothing was flashed. " : "BIOS " + (app.biosInfo.newest || "") + " is staged and is written during the restart. ")
|
||||
+ "Keep the power on and don't touch the machine until it has fully started again, even if the screen stays black.";
|
||||
}
|
||||
Column {
|
||||
anchors.centerIn: parent; width: 760; spacing: 20
|
||||
Rectangle { width: 72; height: 72; radius: 36; anchors.horizontalCenter: parent.horizontalCenter; color: problem ? Theme.warnBg : Theme.goodBg
|
||||
Text { anchors.centerIn: parent; text: problem ? "!" : "✓"; color: problem ? Theme.warn : Theme.good; font.pixelSize: 36; font.weight: Font.Bold } }
|
||||
Text { text: app.runError ? "Nothing changed" : (app.biosRun && app.failed.length ? "The BIOS update stopped" : (app.failed.length ? "Done, with a problem" : "All done"))
|
||||
color: Theme.text; font.family: Theme.display; font.pixelSize: 40; font.weight: Font.DemiBold; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap; color: Theme.soft; font.family: Theme.body; font.pixelSize: 17; text: message }
|
||||
Rectangle { visible: app.failed.length > 0; anchors.horizontalCenter: parent.horizontalCenter; width: ft.implicitWidth + 32; height: 44; radius: 12; color: Theme.warnBg
|
||||
Text { id: ft; anchors.centerIn: parent; color: "#f2c27a"; font.family: Theme.body; font.pixelSize: 14
|
||||
text: "Had a problem: " + app.failed.map(function (id) { return Texts.label(id); }).join(", ") + ". See the details." } }
|
||||
Row { anchors.horizontalCenter: parent.horizontalCenter; spacing: 14; topPadding: 8
|
||||
Btn { k: Input.g.back; text: "Back to the menu"; height: 52; onClicked: app.act("back") }
|
||||
Btn { visible: app.restartNeeded; k: Input.g.ok; text: "Restart now"; primary: true; height: 52; onClicked: app.backend.restart() } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// A button name (A, Enter, OK...), round for a controller's face buttons.
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: glyph
|
||||
property string k
|
||||
property bool dark: false
|
||||
height: 26; width: Math.max(26, gt.implicitWidth + 14)
|
||||
radius: Input.type === "controller" && k.length === 1 ? 13 : 7
|
||||
color: dark ? Theme.ink : Theme.key
|
||||
Text { id: gt; anchors.centerIn: parent; text: glyph.k; color: glyph.dark ? Theme.accent : Theme.textHi
|
||||
font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold }
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Logo, the screen's name (the version on the menu) and the machine.
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
required property AppState app
|
||||
required property string iconUrl
|
||||
readonly property var titles: ({ review: "Review", password: "Password", applying: app.biosRun ? "BIOS update" : "Applying",
|
||||
done: "Done", bios1: "BIOS update", bios2: "BIOS update" })
|
||||
width: parent.width; height: 72; color: Theme.bar
|
||||
Rectangle { anchors.bottom: parent.bottom; width: parent.width; height: 1; color: Theme.line }
|
||||
Row {
|
||||
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 14
|
||||
Image { source: iconUrl; width: 40; height: 40; sourceSize: Qt.size(80, 80); anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: "Steamify"; color: Theme.text; font.family: Theme.display; font.pixelSize: 28; font.weight: Font.Bold; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { readonly property bool menu: app.screen === "menu"
|
||||
text: menu ? "v" + (app.status.version || "") : "/ " + titles[app.screen]
|
||||
color: Theme.faint; font.family: menu ? Theme.mono : Theme.body; font.pixelSize: menu ? 13 : 15; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
Rectangle {
|
||||
visible: app.status.steamMachine === true
|
||||
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter
|
||||
height: 34; radius: 17; color: "#1b2330"; width: mt.implicitWidth + 28
|
||||
Text { id: mt; anchors.centerIn: parent; text: "Steam Machine · CachyOS"; color: Theme.label; font.family: Theme.body; font.pixelSize: 14 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// The input in use (controller, keyboard or remote) and the button names
|
||||
// shown for it.
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
property string type: "keyboard"
|
||||
readonly property var glyphs: ({
|
||||
controller: { toggle: "A", choose: "◀ ▶", reapply: "Y", quit: "B", apply: "X", ok: "A", back: "B" },
|
||||
keyboard: { toggle: "Space", choose: "← →", reapply: "R", quit: "Esc", apply: "Enter", ok: "Enter", back: "Esc" },
|
||||
remote: { toggle: "OK", choose: "◀ ▶", reapply: "Red", quit: "Back", apply: "↓ Apply", ok: "OK", back: "Back" }
|
||||
})
|
||||
readonly property var g: glyphs[type]
|
||||
}
|
||||
+24
-979
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,80 @@
|
||||
// One menu item: name, badge and hint on the left; its control (switch,
|
||||
// Gaming/Desktop choice or action button) on the right. Sub-items fold in
|
||||
// and out with their parent.
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: row
|
||||
required property var modelData
|
||||
required property int index
|
||||
required property AppState app
|
||||
readonly property var it: modelData
|
||||
readonly property bool shown: !it.parent || !!app.want[it.parent]
|
||||
readonly property int rowIndex: app.rows.findIndex(function (r) { return r.id === it.id; })
|
||||
readonly property bool selected: shown && rowIndex === app.sel
|
||||
readonly property bool on: !!app.want[it.id]
|
||||
// Greyed out: an action with nothing to do, or an option this machine
|
||||
// can't turn on.
|
||||
readonly property bool unavailable: (it.kind === "action" && !(app.bios && app.bios.selectable)) || (it.kind === "toggle" && it.selectable === false)
|
||||
readonly property string hint: it.id === "bios" ? app.biosHint()
|
||||
: it.selectable === false ? it.hint
|
||||
: ((Texts.items[it.id] || {}).hint || it.hint)
|
||||
|
||||
height: shown ? 62 : 0
|
||||
opacity: shown ? 1 : 0
|
||||
clip: true
|
||||
Behavior on height { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
|
||||
Behavior on opacity { NumberAnimation { duration: 160 } }
|
||||
|
||||
Rectangle {
|
||||
width: parent.width; height: 56; radius: 12
|
||||
color: row.selected ? Theme.cardSel : Theme.card
|
||||
border.width: row.selected ? 2 : 0; border.color: Theme.accent
|
||||
MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||||
onClicked: { Input.type = "keyboard"; if (row.selected) app.act("accept"); else app.sel = row.rowIndex; } }
|
||||
|
||||
Text { visible: !!row.it.parent; x: 20; anchors.verticalCenter: parent.verticalCenter; text: "└"; color: "#56627a"; font.pixelSize: 18 }
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter; spacing: 2
|
||||
x: row.it.parent ? 46 : 16
|
||||
width: controls.x - x - 16
|
||||
opacity: row.unavailable ? 0.6 : 1
|
||||
Row { width: parent.width; spacing: 10
|
||||
Text { text: app.label(row.it); color: Theme.textHi; font.family: Theme.body; font.pixelSize: 17; font.weight: Font.DemiBold; elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, parent.width - (badge.visible ? badge.width + parent.spacing : 0)) }
|
||||
Badge { id: badge; update: !!row.it.update; isNew: !!row.it["new"]; anchors.verticalCenter: parent.verticalCenter } }
|
||||
Text { text: row.hint; color: Theme.mute; font.family: Theme.body; font.pixelSize: 13; elide: Text.ElideRight; width: parent.width }
|
||||
}
|
||||
|
||||
// Every control ends on the same edge.
|
||||
Item {
|
||||
id: controls
|
||||
opacity: row.it.kind === "toggle" && row.it.selectable === false ? 0.4 : 1
|
||||
anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter
|
||||
width: 180; height: 32
|
||||
// choice
|
||||
Rectangle { visible: row.it.kind === "choice"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||
width: 172; height: 32; radius: 10; color: Theme.bg
|
||||
Row { anchors.centerIn: parent; spacing: 4
|
||||
Repeater { model: [["gamescope", "Gaming"], ["desktop", "Desktop"]]
|
||||
Rectangle { required property var modelData
|
||||
readonly property bool picked: app.boot === modelData[0]
|
||||
width: 80; height: 26; radius: 7; color: picked ? Theme.accent : "transparent"
|
||||
Text { anchors.centerIn: parent; text: parent.modelData[1]; color: parent.picked ? Theme.ink : Theme.mute
|
||||
font.family: Theme.body; font.pixelSize: 13; font.weight: Font.DemiBold }
|
||||
MouseArea { anchors.fill: parent; onClicked: app.boot = parent.modelData[0] } } } } }
|
||||
// toggle: "now on/off" left of the switch
|
||||
Rectangle { id: sw; visible: row.it.kind === "toggle"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||
width: 46; height: 26; radius: 13; color: row.on ? Theme.accent : "#343f50"
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
Rectangle { width: 20; height: 20; radius: 10; color: "#f4f7fb"; y: 3; x: row.on ? 23 : 3; Behavior on x { NumberAnimation { duration: 120 } } } }
|
||||
Text { visible: row.it.kind === "toggle"; anchors.right: sw.left; anchors.rightMargin: 12; anchors.verticalCenter: parent.verticalCenter
|
||||
text: row.it.on ? "now on" : "now off"; color: row.it.on ? Theme.good : Theme.faint; font.family: Theme.mono; font.pixelSize: 12 }
|
||||
// action (Update BIOS)
|
||||
Rectangle { visible: row.it.kind === "action"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
|
||||
width: at.implicitWidth + 24; height: 30; radius: 8; color: Theme.warnBg
|
||||
Text { id: at; anchors.centerIn: parent; color: Theme.warn; font.family: Theme.body; font.pixelSize: 13; font.weight: Font.DemiBold
|
||||
text: app.biosChecking ? "Checking…" : app.bios && app.bios.selectable ? "Update…" : (app.bios && app.bios.newest ? "Up to date" : "Unavailable") } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// The menu: every item on the left, what the selected one does and the
|
||||
// system on the right, Review & apply at the bottom.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Item {
|
||||
id: screen
|
||||
required property AppState app
|
||||
readonly property var g: Input.g
|
||||
|
||||
Text {
|
||||
visible: app.items.length === 0
|
||||
anchors.centerIn: parent; color: Theme.mute; font.family: Theme.body; font.pixelSize: 18
|
||||
text: app.status.error ? "Couldn't read the current state:\n" + app.status.error : "Checking what's on…"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: app.items.length > 0
|
||||
x: 40; y: 28; width: parent.width - 80; height: parent.height - 28 - 64 - 20
|
||||
Column {
|
||||
width: 740; height: parent.height; spacing: 10
|
||||
Text { text: "What do you want?"; color: Theme.text; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold }
|
||||
ListView {
|
||||
id: list; width: parent.width; height: parent.height - 50; clip: true
|
||||
// A fixed model: sub-items fold in and out instead of the
|
||||
// list being rebuilt (and jumping) on every tick.
|
||||
model: app.items
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
delegate: MenuRow {
|
||||
app: screen.app
|
||||
width: list.width - 12; x: 2
|
||||
onSelectedChanged: if (selected) Qt.callLater(list.positionViewAtIndex, index, ListView.Contain)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
x: 772; width: parent.width - 772; height: parent.height; spacing: 16
|
||||
DetailPanel { app: screen.app; width: parent.width; height: parent.height - sys.height - 16 }
|
||||
Rectangle {
|
||||
id: sys; width: parent.width; height: 82; radius: 16; color: Theme.card
|
||||
Grid { anchors.fill: parent; anchors.margins: 16; anchors.leftMargin: 20; anchors.rightMargin: 20; columns: 2; columnSpacing: 20; rowSpacing: 10
|
||||
Repeater {
|
||||
model: [["Kernel", (app.status.kernel || "").replace("-cachyos", "")],
|
||||
["LED bar", app.status.steamMachine ? (app.status.leds || 0) + " LEDs" : "—"],
|
||||
["HDMI-CEC", app.status.cecDevices ? "/dev/" + app.status.cecDevices.split(" ")[0] : "none"],
|
||||
["Version", app.status.version || ""]]
|
||||
Row { required property var modelData; width: (sys.width - 60) / 2
|
||||
Text { text: parent.modelData[0]; color: Theme.faint; font.family: Theme.body; font.pixelSize: 13; width: parent.width / 2 }
|
||||
Text { text: parent.modelData[1]; color: Theme.text; font.family: Theme.mono; font.pixelSize: 13; width: parent.width / 2
|
||||
horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bar {
|
||||
Row {
|
||||
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 28
|
||||
Repeater {
|
||||
model: [[screen.g.toggle, "Toggle"], [screen.g.choose, "Choose"], [screen.g.reapply, "Re-apply what's on"], [screen.g.quit, "Quit"]]
|
||||
Row { required property var modelData; spacing: 8
|
||||
Glyph { k: parent.modelData[0]; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: parent.modelData[1]; color: Theme.label; font.family: Theme.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter } }
|
||||
}
|
||||
}
|
||||
Row {
|
||||
anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 16
|
||||
Text { readonly property int n: app.computePlan().length
|
||||
text: n === 0 ? "Everything is the way you want it" : n + (n === 1 ? " change" : " changes")
|
||||
color: Theme.faint; font.family: Theme.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter }
|
||||
Btn { k: screen.g.apply; text: "Review & apply"; primary: true; focusRing: app.sel === app.rows.length; height: 44; onClicked: app.goReview(false) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// The sudo password, asked once per run; never stored.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Rectangle {
|
||||
id: screen
|
||||
required property AppState app
|
||||
color: "#0c1016"
|
||||
function submit() { var p = pw.text; pw.text = ""; app.submitPassword(p); }
|
||||
|
||||
Connections {
|
||||
target: screen.app
|
||||
function onPasswordRequested() { pw.text = ""; pw.shown = false; pw.forceActiveFocus(); }
|
||||
function onPasswordRejected() { pw.forceActiveFocus(); }
|
||||
function onPasswordSubmitRequested() { screen.submit(); }
|
||||
}
|
||||
Binding { target: screen.app; property: "typing"; value: pw.activeFocus; when: screen.visible }
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent; width: 560; height: col.implicitHeight + 72; radius: 20; color: Theme.card; border.width: 1; border.color: Theme.line
|
||||
Column {
|
||||
id: col; anchors.fill: parent; anchors.margins: 36; anchors.leftMargin: 40; anchors.rightMargin: 40; spacing: 18
|
||||
Row { spacing: 14
|
||||
Rectangle { width: 48; height: 48; radius: 14; color: Theme.infoBg
|
||||
// A padlock.
|
||||
Canvas { anchors.centerIn: parent; width: 24; height: 24
|
||||
onPaint: { var c = getContext("2d"); c.strokeStyle = Theme.info; c.lineWidth = 1.8; c.beginPath(); c.roundedRect(4.5, 10.5, 15, 10, 2.5, 2.5); c.stroke();
|
||||
c.beginPath(); c.moveTo(8, 10.5); c.lineTo(8, 8); c.arc(12, 8, 4, Math.PI, 0); c.lineTo(16, 10.5); c.stroke(); } } }
|
||||
Column { spacing: 2; anchors.verticalCenter: parent.verticalCenter
|
||||
Text { text: "Your password, once"; color: Theme.text; font.family: Theme.display; font.pixelSize: 30; font.weight: Font.DemiBold }
|
||||
Text { text: "Steamify needs it to change system settings."; color: Theme.mute; font.family: Theme.body; font.pixelSize: 14 } }
|
||||
}
|
||||
Text { text: "Password for " + app.backend.user(); color: Theme.soft; font.family: Theme.body; font.pixelSize: 14; font.weight: Font.DemiBold }
|
||||
TextField {
|
||||
id: pw; width: parent.width; height: 52; echoMode: shown ? TextInput.Normal : TextInput.Password
|
||||
property bool shown: false
|
||||
color: Theme.textHi; font.family: Theme.body; font.pixelSize: 18; leftPadding: 16; rightPadding: 56
|
||||
placeholderText: "Password"; placeholderTextColor: "#6b778a"
|
||||
background: Rectangle { radius: 12; color: Theme.bg; border.width: 2; border.color: app.wrongPassword ? Theme.bad : Theme.accent }
|
||||
Keys.onReturnPressed: screen.submit()
|
||||
Keys.onEnterPressed: screen.submit()
|
||||
Keys.onEscapePressed: app.act("back")
|
||||
Text { anchors.right: parent.right; anchors.rightMargin: 16; anchors.verticalCenter: parent.verticalCenter; text: pw.shown ? "Hide" : "Show"
|
||||
color: Theme.label; font.family: Theme.body; font.pixelSize: 13
|
||||
MouseArea { anchors.fill: parent; anchors.margins: -10; onClicked: pw.shown = !pw.shown } }
|
||||
}
|
||||
Text { visible: app.wrongPassword; text: "That password didn't work. Try again."; color: Theme.bad; font.family: Theme.body; font.pixelSize: 14 }
|
||||
Rectangle { width: parent.width; height: 46; radius: 12; color: Theme.note
|
||||
Text { anchors.verticalCenter: parent.verticalCenter; x: 14; color: Theme.soft; font.family: Theme.body; font.pixelSize: 14
|
||||
text: ({ controller: "Press Steam + X for the on-screen keyboard.", keyboard: "Type your password and press Enter.", remote: "Select the field for the on-screen keyboard." })[Input.type] } }
|
||||
Item { width: parent.width; height: 48
|
||||
Btn { k: Input.g.back; text: "Back"; onClicked: app.act("back") }
|
||||
Btn { anchors.right: parent.right; k: Input.g.ok; text: "Apply"; primary: true; onClicked: screen.submit() } }
|
||||
Text { text: "Only used for this run. Steamify never stores it."; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; anchors.horizontalCenter: parent.horizontalCenter }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// What a run will change, with a summary; Apply asks for the password.
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
Item {
|
||||
required property AppState app
|
||||
Column {
|
||||
x: 40; y: 32; width: 740; spacing: 14
|
||||
Text { text: app.plan.length === 0 ? "Nothing to change" : "This will"; color: Theme.text; font.family: Theme.display; font.pixelSize: 34; font.weight: Font.DemiBold }
|
||||
ListView {
|
||||
width: 740; clip: true; spacing: 8
|
||||
height: Math.min(contentHeight, parent.parent.height - 32 - 50 - 14 - 66 - 64 - 32)
|
||||
model: app.plan
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
delegate: Rectangle {
|
||||
id: step
|
||||
required property var modelData
|
||||
readonly property var st: Texts.actions[modelData.action] || ["", Theme.text, Theme.card]
|
||||
width: 740; height: 56; radius: 12; color: Theme.card
|
||||
Row { anchors.fill: parent; anchors.leftMargin: 18; spacing: 14
|
||||
Chip { text: step.st[0]; fg: step.st[1]; bgc: step.st[2]; width: 84; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: Texts.label(step.modelData.id); color: Theme.textHi; font.family: Theme.body; font.pixelSize: 17; font.weight: Font.DemiBold
|
||||
anchors.verticalCenter: parent.verticalCenter }
|
||||
Badge { isNew: !!step.modelData.isNew; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle { width: 740; height: 52; radius: 12; color: Theme.note
|
||||
Text { anchors.verticalCenter: parent.verticalCenter; x: 18; text: "Your password is asked once. Changes to how the PC starts need a restart."
|
||||
color: Theme.soft; font.family: Theme.body; font.pixelSize: 14 } }
|
||||
}
|
||||
Rectangle {
|
||||
x: parent.width - 468; y: 32; width: 428; height: 250; radius: 16; color: Theme.card
|
||||
Column { anchors.fill: parent; anchors.margins: 24; spacing: 12
|
||||
Text { text: "SUMMARY"; color: Theme.faint; font.family: Theme.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.8 }
|
||||
Repeater { model: [["Turn on", "on"], ["Update", "update"], ["Re-apply", "again"], ["Turn off", "off"]]
|
||||
Row { required property var modelData; width: 380
|
||||
Text { text: parent.modelData[0]; color: Theme.soft; font.family: Theme.body; font.pixelSize: 15; width: 300 }
|
||||
Text { readonly property string action: parent.modelData[1]
|
||||
text: app.plan.filter(function (p) { return p.action === action; }).length
|
||||
color: Theme.text; font.family: Theme.mono; font.pixelSize: 15; width: 80; horizontalAlignment: Text.AlignRight } } }
|
||||
}
|
||||
}
|
||||
Bar {
|
||||
Btn { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: Input.g.back; text: "Back to the menu"; onClicked: app.act("back") }
|
||||
Btn { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; k: Input.g.ok; text: "Apply"; primary: true
|
||||
visible: app.plan.length > 0; onClicked: app.onApplyPressed() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// The BIOS update's risks.
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: list
|
||||
spacing: 8
|
||||
Repeater {
|
||||
model: ["A failed or interrupted BIOS update can leave the machine unable to start (bricked). Steamify, CachyOS and Valve take no responsibility for that.",
|
||||
"NEVER turn off the power, unplug the machine or press the power button while it updates, including the restart(s) afterwards.",
|
||||
"The screen can stay black for several minutes. Wait.",
|
||||
"Only on mains power, with all other programs closed."]
|
||||
Row { required property string modelData; spacing: 10; width: list.width
|
||||
Rectangle { width: 6; height: 6; radius: 3; color: Theme.bad; y: 9 }
|
||||
Text { text: parent.modelData; width: parent.width - 16; wrapMode: Text.WordWrap; color: Theme.soft
|
||||
font.family: Theme.body; font.pixelSize: 15; lineHeight: 1.3 } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Texts per item; the backend's labels are the fallback.
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
readonly property var items: ({
|
||||
gaming: { label: "SteamOS conversion", hint: "Boot into gaming mode, Steam on the desktop",
|
||||
body: "Boots straight into gaming mode. Switch to Desktop in Steam works, and Return to Gaming Mode on the desktop brings you back.",
|
||||
changes: ["Autologin into gamescope", "Return to Gaming Mode shortcut", "Steam starts silently on the desktop"] },
|
||||
boot: { label: "Boot into", hint: "Where the PC starts",
|
||||
body: "Where the PC starts after a restart. Switching back and forth works the same either way.",
|
||||
changes: ["Desktop: the Plasma session is set before login", "Gaming: the SteamOS default"] },
|
||||
theme: { label: "SteamOS theme", hint: "Vapor look for the desktop, Add to Steam",
|
||||
body: "The Vapor look of SteamOS for the desktop, with its panel, launcher icon and wallpaper.",
|
||||
changes: ["cachyos-vapor global theme", "Add to Steam in right-click menus", "Nested Desktop, Steam keyboard window rule"] },
|
||||
glyphs: { label: "Steam Deck/Machine icons", hint: "Deck button icons in gaming mode",
|
||||
body: "Steam shows Steam Deck button icons in gaming mode.", changes: ["STEAM_GAMEPADUI_ARGS -steamos3"] },
|
||||
single: { label: "Single user mode", hint: "No password, lock screen or wallet prompts",
|
||||
body: "Like SteamOS: never a login or lock screen, and no KDE wallet password prompts. Typing a password with a controller is no fun.",
|
||||
changes: ["SDDM autologin, no lock screen or user switching", "Valve's empty wallet; your own is kept aside", "Launcher shows Sleep, Restart, Shut Down only"] },
|
||||
launcher: { label: "Steamify shortcut", hint: "The app on the desktop and in the launcher",
|
||||
body: "Opens the newest Steamify app, so you never need the install command again. Steamify Terminal in the launcher opens the terminal menu.",
|
||||
changes: ["Steamify CachyOS on the desktop and in the launcher (the app)", "Steamify Terminal in the launcher"] },
|
||||
vram: { label: "VRAM booster", hint: "The game in front keeps its VRAM",
|
||||
body: "Like SteamOS 3.9: the game you're playing keeps its video memory, and background apps are moved out first. Without it a game that needs most of the video memory can spill into system RAM and stutter. Shown for GPUs whose driver supports it (AMD, Intel) on kernel 7.2 or newer.",
|
||||
changes: ["dmemcg-booster (system and user service)", "plasma-foreground-booster, which tells it which window is in front", "Off: removed again, unless you had installed them yourself"] },
|
||||
cec: { label: "HDMI-CEC", hint: "Use Steam with the TV remote, TV on/off with the PC", experimental: true,
|
||||
body: "Use Steam with your TV remote, and the TV turns on and off with the PC. Turn on CEC on the TV too (Sony: BRAVIA Sync, Samsung: Anynet+, LG: SimpLink).",
|
||||
changes: ["Valve's cecd and cec-audio-control", "HDMI CEC section in Steam's Display settings", "Volume buttons for the TV in the Quick Access menu"] },
|
||||
machine: { label: "Steam Machine support", hint: "LED bar, fan and performance settings in Steam",
|
||||
body: "The front LED bar works, Steam's hardware settings work, and the power button puts it to sleep like a console.",
|
||||
changes: ["leds-valve driver for every kernel (DKMS)", "steamos-manager for Steam's settings", "Console-like power handling"] },
|
||||
poweroff: { label: "Power-off fix", hint: "Stays off after shutting down",
|
||||
body: "With recent kernels the Steam Machine starts again right after shutting down: the firmware leaves a wake bit set, and newer kernels (7.2, and updates of 6.x, 7.0 and 7.1) no longer clear it. Valve's own kernel clears it; this small module does the same right before power-off.",
|
||||
changes: ["steamify-fremont-poweroff module for each installed kernel (DKMS)", "Only on a Steam Machine, only touches that one wake bit", "Off: recent kernels may start it again after shutting down"] },
|
||||
kpin: { label: "Pin the kernel", hint: "No longer needed: untick for CachyOS's current kernel",
|
||||
body: "Kept the Steam Machine on CachyOS kernel 7.1.6, which powered off properly. The power-off fix does that on every kernel now, so the pin is removed and CachyOS's current kernel comes back.",
|
||||
changes: ["Removed from IgnorePkg", "linux-cachyos updated to CachyOS's current version"] },
|
||||
hdmi: { label: "HDMI refresh boost", hint: "No longer needed: removed",
|
||||
body: "Made HDMI displays run above 60 Hz on the pinned kernel. Current kernels do that themselves, so what's left of it is removed and every display uses its own EDID again.",
|
||||
changes: ["Saved display EDIDs and the hotplug service removed", "The old kernel parameter removed, if it's still there"] },
|
||||
bios: { label: "Update BIOS", hint: "",
|
||||
body: "Installs Valve's newest Steam Machine BIOS, at your own risk. It checks Valve's checksum and asks fwupd whether the file fits this machine, then warns you twice before anything is written.",
|
||||
changes: ["Valve's fremont-hw-support package (checksum checked)", "fwupd writes the BIOS during the next restart", "Keep the power on until the machine has fully started again"] }
|
||||
})
|
||||
// Replaces the explanation of an option that can't be turned on here.
|
||||
readonly property var unsupported: ({
|
||||
vram: { body: "Not available with your NVIDIA graphics card: NVIDIA's driver doesn't tell Linux how its video memory is used yet, so there's nothing to steer. It works with AMD and Intel graphics cards, and shows up here as soon as NVIDIA's driver supports it.",
|
||||
changes: ["Nothing: can't be turned on with NVIDIA's driver"] }
|
||||
})
|
||||
|
||||
// Per plan action: the review's chip [text, colour, background] and the
|
||||
// progress screen's verb.
|
||||
readonly property var actions: ({
|
||||
on: ["Turn on", Theme.good, Theme.goodBg, "Turn on "], off: ["Turn off", Theme.bad, Theme.badBg, "Turn off "],
|
||||
again: ["Re-apply", Theme.info, Theme.infoBg, "Re-apply "], update: ["Update", Theme.warn, Theme.warnBg, ""],
|
||||
desktop: ["Desktop", Theme.info, Theme.infoBg, "Boot into "], gaming: ["Gaming", Theme.info, Theme.infoBg, "Boot into "],
|
||||
check: ["Check", Theme.warn, Theme.warnBg, "Download and check the "], flash: ["Flash", Theme.bad, Theme.badBg, "Hand to fwupd: the "]
|
||||
})
|
||||
|
||||
function of(item) {
|
||||
var tx = items[item.id] || {};
|
||||
return item.selectable === false && unsupported[item.id] ? Object.assign({}, tx, unsupported[item.id]) : tx;
|
||||
}
|
||||
function label(id, fallback) { return (items[id] || {}).label || fallback || id; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Colours and fonts (from the design).
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
readonly property color bg: "#0f1319"
|
||||
readonly property color bar: "#141a22"
|
||||
readonly property color line: "#232c38"
|
||||
readonly property color card: "#161d27"
|
||||
readonly property color cardSel: "#1f2a3a"
|
||||
readonly property color text: "#e6ebf2"
|
||||
readonly property color textHi: "#eef2f7"
|
||||
readonly property color soft: "#c3ccd8"
|
||||
readonly property color mute: "#97a3b4"
|
||||
readonly property color faint: "#8b97a8"
|
||||
readonly property color label: "#b8c3d1"
|
||||
readonly property color accent: "#3ea6ff"
|
||||
readonly property color ink: "#06121f"
|
||||
readonly property color good: "#7fd1a8"
|
||||
readonly property color goodBg: "#173226"
|
||||
readonly property color warn: "#f2a33a"
|
||||
readonly property color warnBg: "#2a2116"
|
||||
readonly property color bad: "#ff9a8a"
|
||||
readonly property color badBg: "#2a1c1c"
|
||||
readonly property color info: "#7cc4ff"
|
||||
readonly property color infoBg: "#1b2b40"
|
||||
readonly property color note: "#1a2230"
|
||||
readonly property color key: "#2a3444"
|
||||
readonly property color button: "#232c38"
|
||||
readonly property color danger: "#8a2c2c"
|
||||
readonly property color dangerBg: "#1d1416"
|
||||
readonly property color dangerLine: "#b33a3a"
|
||||
readonly property string display: "Barlow Semi Condensed"
|
||||
readonly property string body: "IBM Plex Sans"
|
||||
readonly property string mono: "IBM Plex Mono"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
singleton Theme 1.0 Theme.qml
|
||||
singleton Texts 1.0 Texts.qml
|
||||
singleton Input 1.0 Input.qml
|
||||
AppState 1.0 AppState.qml
|
||||
Glyph 1.0 Glyph.qml
|
||||
Btn 1.0 Btn.qml
|
||||
Chip 1.0 Chip.qml
|
||||
Badge 1.0 Badge.qml
|
||||
Bar 1.0 Bar.qml
|
||||
RiskList 1.0 RiskList.qml
|
||||
Header 1.0 Header.qml
|
||||
MenuRow 1.0 MenuRow.qml
|
||||
DetailPanel 1.0 DetailPanel.qml
|
||||
MenuScreen 1.0 MenuScreen.qml
|
||||
ReviewScreen 1.0 ReviewScreen.qml
|
||||
PasswordScreen 1.0 PasswordScreen.qml
|
||||
ApplyingScreen 1.0 ApplyingScreen.qml
|
||||
BiosWarningScreen 1.0 BiosWarningScreen.qml
|
||||
BiosConfirmScreen 1.0 BiosConfirmScreen.qml
|
||||
DoneScreen 1.0 DoneScreen.qml
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
# 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 and lib/) to ~/.local/share/steamify/app, installs PySide6 if
|
||||
# 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>
|
||||
|
||||
+36
-27
@@ -197,19 +197,19 @@ class Backend(QObject):
|
||||
p.start()
|
||||
|
||||
def _make_askpass(self, password):
|
||||
"""A private folder with the password, a helper that prints it for
|
||||
`sudo -A`, and a `sudo` that always uses the helper (makepkg and
|
||||
yay call plain sudo)."""
|
||||
"""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)
|
||||
secret = os.path.join(d, "secret")
|
||||
fd = os.open(secret, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(fd, "w") as f:
|
||||
f.write(password + "\n")
|
||||
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(secret))
|
||||
f.write("#!/bin/sh\nexec cat %s\n" % json.dumps(pipe))
|
||||
os.chmod(askpass, 0o700)
|
||||
bindir = os.path.join(d, "bin")
|
||||
os.mkdir(bindir)
|
||||
@@ -217,9 +217,35 @@ class Backend(QObject):
|
||||
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
|
||||
@@ -245,8 +271,8 @@ class Backend(QObject):
|
||||
self.busyChanged.emit()
|
||||
p.start()
|
||||
|
||||
@Slot("QVariant", str, bool, str, str)
|
||||
def apply(self, ids, boot, reapply, password, hdmi):
|
||||
@Slot("QVariant", str, bool, str)
|
||||
def apply(self, ids, boot, reapply, password):
|
||||
try:
|
||||
if hasattr(ids, "toVariant"): # a JS array from QML
|
||||
ids = ids.toVariant()
|
||||
@@ -255,8 +281,6 @@ class Backend(QObject):
|
||||
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.
|
||||
@@ -274,21 +298,6 @@ class Backend(QObject):
|
||||
"""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)
|
||||
|
||||
def _read(self, p):
|
||||
self._buf += bytes(p.readAllStandardOutput())
|
||||
while b"\n" in self._buf:
|
||||
|
||||
Reference in new issue
Block a user