feat: BIOS update in the app, with both warnings

bios_enable split into bios_prepare (download, checksum, fwupd check) and
bios_flash; the terminal flow is unchanged. The app: check, warning 1,
warning 2 (type UPDATE, or hold A/OK for 5 seconds), flash, restart.
This commit is contained in:
theupriser committed 2026-09-25 15:53:30 +02:00
1 parent 396459b511
commit 7c48b3d806
4 files changed
+298 -85

No files matched your search

+49 -15
View File
@@ -8,7 +8,9 @@
# turns on the listed components and off the others (like ticking them # turns on the listed components and off the others (like ticking them
# in the menu), then streams one JSON object per line: plan, start, # in the menu), then streams one JSON object per line: plan, start,
# log, done, finished. sudo asks through $SUDO_ASKPASS (no terminal). # log, done, finished. sudo asks through $SUDO_ASKPASS (no terminal).
# The BIOS update needs its typed confirmations: it stays in the terminal. # steamify.sh --backend bios-prepare | bios-flash
# the BIOS update in two steps, so the app shows both warnings between
# them (bios-ready event: current, newest, checksum, compatible).
# Sourced by steamify.sh; not meant to be run on its own. # Sourced by steamify.sh; not meant to be run on its own.
json_str() { json_str() {
@@ -54,13 +56,17 @@ backend_status() {
done done
local cec="" f local cec="" f
for f in /dev/cec*; do [[ -e "$f" ]] && cec+="${cec:+ }$(basename "$f")"; done for f in /dev/cec*; do [[ -e "$f" ]] && cec+="${cec:+ }$(basename "$f")"; done
printf '{"version":%s,"firstRun":%s,"steamMachine":%s,"kernel":%s,"pinnedKernel":%s,"cecDevices":%s,"leds":%s,"items":[%s]}\n' \ local bios='null'
if bios_available; then
bios="{\"current\":$(json_str "$(bios_current)"),\"newest\":$(json_str "${BIOS_NEWEST:-}"),\"selectable\":$(bios_selectable && echo true || echo false),\"dryRun\":$([[ -n "$BIOS_DRY_RUN" ]] && echo true || echo false)}"
fi
printf '{"version":%s,"firstRun":%s,"steamMachine":%s,"kernel":%s,"pinnedKernel":%s,"cecDevices":%s,"leds":%s,"bios":%s,"items":[%s]}\n' \
"$(json_str "$VERSION")" "$first_run" \ "$(json_str "$VERSION")" "$first_run" \
"$(detect_valve_fremont && echo true || echo false)" \ "$(detect_valve_fremont && echo true || echo false)" \
"$(json_str "$(uname -r)")" "$(json_str "${PINNED_KERNEL_VER:-}")" \ "$(json_str "$(uname -r)")" "$(json_str "${PINNED_KERNEL_VER:-}")" \
"$(json_str "$cec")" \ "$(json_str "$cec")" \
"$(compgen -G '/sys/class/leds/valve-leds*' | wc -l)" \ "$(compgen -G '/sys/class/leds/valve-leds*' | wc -l)" \
"$items" "$bios" "$items"
} }
backend_run_component() { backend_run_component() {
@@ -77,6 +83,43 @@ backend_run_component() {
return $rc return $rc
} }
backend_sudo() {
# sudo from the app has no terminal: ask through its helper, and keep the
# credentials fresh as the menu does.
if [[ -z "${SUDO_ASKPASS:-}" ]] && ! sudo -n true 2>/dev/null; then
backend_event finished '"failed":[],"restart":false,"error":"no-sudo"'
return 1
fi
sudo() { command sudo ${SUDO_ASKPASS:+-A} "$@"; }
if ! sudo -n true 2>/dev/null && ! sudo true 2>/dev/null; then
backend_event finished '"failed":[],"restart":false,"error":"wrong-password"'
return 1
fi
while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null &
}
backend_bios_prepare() {
# Download, checksum and fwupd's device check; the app then shows its
# two warnings and calls bios-flash.
local rc
backend_sudo || return 1
backend_event start '"id":"bios","action":"prepare","label":"Update BIOS"'
bios_prepare > >(while IFS= read -r line; do backend_event log "\"id\":\"bios\",\"line\":$(json_str "$line")"; done) 2>&1
rc=$?; wait $! 2>/dev/null
case $rc in
0) backend_event bios-ready "\"current\":$(json_str "$(bios_current)"),\"newest\":$(json_str "$BIOS_NEWEST"),\"checksum\":true,\"compatible\":$(json_str "$BIOS_COMPATIBLE")" ;;
2) backend_event finished '"failed":[],"restart":false,"nothing":true' ;;
*) backend_event finished '"failed":["bios"],"restart":false' ;;
esac
}
backend_bios_flash() {
backend_sudo || return 1
local rc
backend_run_component bios flash_only; rc=$?
backend_event finished "\"failed\":$([[ $rc -eq 0 ]] && echo '[]' || echo '["bios"]'),\"restart\":$(restart_needed && echo true || echo false),\"bios\":true"
}
backend_apply() { backend_apply() {
local reapply=false boot="" id c local reapply=false boot="" id c
local -a want=() local -a want=()
@@ -111,18 +154,7 @@ backend_apply() {
return 0 return 0
fi fi
# sudo from the app has no terminal: ask through its helper, and keep the backend_sudo || return 1
# credentials fresh as the menu does.
if [[ -z "${SUDO_ASKPASS:-}" ]] && ! sudo -n true 2>/dev/null; then
backend_event finished '"failed":[],"restart":false,"error":"no-sudo"'
return 1
fi
sudo() { command sudo ${SUDO_ASKPASS:+-A} "$@"; }
if ! sudo -n true 2>/dev/null && ! sudo true 2>/dev/null; then
backend_event finished '"failed":[],"restart":false,"error":"wrong-password"'
return 1
fi
while true; do sudo -n true; sleep 50; kill -0 "$$" 2>/dev/null || exit; done 2>/dev/null &
LOGIN_MANAGER=plasmalogin LOGIN_MANAGER=plasmalogin
[[ "${WANTED[single]}" == 1 ]] && LOGIN_MANAGER=sddm [[ "${WANTED[single]}" == 1 ]] && LOGIN_MANAGER=sddm
@@ -137,6 +169,8 @@ backend_main() {
case "${1:-}" in case "${1:-}" in
status) backend_status ;; status) backend_status ;;
apply) shift; backend_apply "$@" ;; apply) shift; backend_apply "$@" ;;
bios-prepare) backend_bios_prepare ;;
bios-flash) backend_bios_flash ;;
*) err "Usage: steamify.sh --backend status | apply [--reapply] [--boot gamescope|desktop] <id>..."; return 2 ;; *) err "Usage: steamify.sh --backend status | apply [--reapply] [--boot gamescope|desktop] <id>..."; return 2 ;;
esac esac
} }
+74 -44
View File
@@ -121,54 +121,102 @@ bios_fits_device() {
grep -q '"Guid"' <<< "$details" && ! grep -q '"UpdateError"' <<< "$details" grep -q '"Guid"' <<< "$details" && ! grep -q '"UpdateError"' <<< "$details"
} }
bios_enable() { # The checked firmware waits here between bios_prepare and bios_flash, so the
# app can show its warnings in between (and the terminal flow uses it too).
BIOS_STAGE_DIR="${XDG_RUNTIME_DIR:-/tmp}/steamify-bios"
bios_prepare() {
# Downloads Valve's package, checks its SHA-256 and extracts the .cab to
# $BIOS_STAGE_DIR, then asks fwupd whether it fits this machine. Sets
# BIOS_COMPATIBLE (yes|dry-run). 0 = ready to flash, 2 = nothing to do
# (already the newest), 1 = error (nothing was touched).
if ! bios_lookup_newest; then if ! bios_lookup_newest; then
err "Couldn't find the newest Steam Machine BIOS on Valve's mirror ($VALVE_MIRROR)." err "Couldn't find the newest Steam Machine BIOS on Valve's mirror ($VALVE_MIRROR)."
return 1 return 1
fi fi
local current tmp compatible
current="$(bios_current)"
[[ -n "$BIOS_DRY_RUN" ]] && warn "DRY RUN (WIZARD_BIOS_DRY_RUN): nothing will be flashed." [[ -n "$BIOS_DRY_RUN" ]] && warn "DRY RUN (WIZARD_BIOS_DRY_RUN): nothing will be flashed."
if [[ "$current" == "$BIOS_NEWEST" ]]; then if [[ "$(bios_current)" == "$BIOS_NEWEST" ]]; then
ok "The BIOS is already the newest version ($current); nothing to do." ok "The BIOS is already the newest version ($BIOS_NEWEST); nothing to do."
return 0 return 2
fi fi
# Download and check everything before asking: the warnings only come
# when the file is Valve's (SHA-256 from Valve's repo database) and fwupd
# confirms it's firmware for this very machine.
pacman -Q fwupd >/dev/null 2>&1 || sudo pacman -S --needed --noconfirm fwupd || pacman -Q fwupd >/dev/null 2>&1 || sudo pacman -S --needed --noconfirm fwupd ||
{ err "Installing fwupd failed."; return 1; } { err "Installing fwupd failed."; return 1; }
tmp="$(mktemp -d)" rm -rf "$BIOS_STAGE_DIR"; mkdir -p "$BIOS_STAGE_DIR"; chmod 700 "$BIOS_STAGE_DIR"
info "Downloading $BIOS_PKG ($BIOS_REPO)..." info "Downloading $BIOS_PKG ($BIOS_REPO)..."
if ! curl -fsSL "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_PKG" -o "$tmp/pkg.tar.zst" || if ! curl -fsSL "$VALVE_MIRROR/$BIOS_REPO/os/x86_64/$BIOS_PKG" -o "$BIOS_STAGE_DIR/pkg.tar.zst" ||
! echo "$BIOS_SHA256 $tmp/pkg.tar.zst" | sha256sum -c --quiet - || ! echo "$BIOS_SHA256 $BIOS_STAGE_DIR/pkg.tar.zst" | sha256sum -c --quiet - ||
! tar -I unzstd -xf "$tmp/pkg.tar.zst" -C "$tmp" "$BIOS_CAB"; then ! tar -I unzstd -xf "$BIOS_STAGE_DIR/pkg.tar.zst" -C "$BIOS_STAGE_DIR" "$BIOS_CAB"; then
err "Downloading or verifying $BIOS_PKG failed; the BIOS was not touched." err "Downloading or verifying $BIOS_PKG failed; the BIOS was not touched."
rm -rf "$tmp" rm -rf "$BIOS_STAGE_DIR"
return 1 return 1
fi fi
ok "Checksum OK: this is Valve's $BIOS_PKG." ok "Checksum OK: this is Valve's $BIOS_PKG."
local yes="$c_green$c_bold" b="$c_bold" n="$c_reset"
if [[ -n "$BIOS_DRY_RUN" ]]; then if [[ -n "$BIOS_DRY_RUN" ]]; then
warn "Dry run: skipping fwupd's check that the firmware fits this machine." warn "Dry run: skipping fwupd's check that the firmware fits this machine."
compatible="${c_yellow}${b}not checked${n} (dry run)" BIOS_COMPATIBLE=dry-run
elif bios_fits_device "$tmp/$BIOS_CAB"; then elif bios_fits_device "$BIOS_STAGE_DIR/$BIOS_CAB"; then
ok "fwupd confirms BIOS $BIOS_NEWEST is firmware for this machine." ok "fwupd confirms BIOS $BIOS_NEWEST is firmware for this machine."
compatible="${yes}yes${n} (checked by fwupd)" BIOS_COMPATIBLE=yes
else else
err "fwupd says BIOS $BIOS_NEWEST is not for this machine's hardware; not installing it." err "fwupd says BIOS $BIOS_NEWEST is not for this machine's hardware; not installing it."
rm -rf "$tmp" rm -rf "$BIOS_STAGE_DIR"
return 1 return 1
fi fi
printf '%s\n' "$BIOS_NEWEST" "$BIOS_CAB" > "$BIOS_STAGE_DIR/ready"
}
bios_flash() {
# Hands the firmware bios_prepare checked to fwupd; it is written during
# the next restart. Sets BIOS_NEEDS_RESTART.
local newest cab
{ read -r newest; read -r cab; } < "$BIOS_STAGE_DIR/ready" 2>/dev/null
if [[ -z "${cab:-}" || ! -f "$BIOS_STAGE_DIR/$cab" ]]; then
err "No checked BIOS file waiting; nothing was flashed."
return 1
fi
if [[ -n "$BIOS_DRY_RUN" ]]; then
ok "Dry run: would run: fwupdmgr install -y --no-reboot-check $(basename "$cab")"
ok "Dry run finished; nothing was flashed."
# Treated as staged, so the restart choices that follow a real
# update show up too; restarting only prints (see restart_now).
BIOS_NEEDS_RESTART=1
rm -rf "$BIOS_STAGE_DIR"
return 0
fi
info "Handing BIOS $newest to fwupd. Do NOT turn off the power from now on."
# -y: the user confirmed twice; --no-reboot-check: our own restart
# question comes at the end.
if ! sudo fwupdmgr install -y --no-reboot-check "$BIOS_STAGE_DIR/$cab"; then
err "fwupd could not install the BIOS update (see above); the BIOS was not changed."
rm -rf "$BIOS_STAGE_DIR"
return 1
fi
rm -rf "$BIOS_STAGE_DIR"
BIOS_NEEDS_RESTART=1
ok "BIOS $newest is staged. It is written during the next restart:"
warn "keep the power on and don't touch the machine until it has fully started again."
}
bios_enable() {
local rc current compatible
current="$(bios_current)"
bios_prepare; rc=$?
[[ $rc -eq 2 ]] && return 0
[[ $rc -ne 0 ]] && return 1
local yes="$c_green$c_bold" b="$c_bold" n="$c_reset"
if [[ "$BIOS_COMPATIBLE" == dry-run ]]; then
compatible="${c_yellow}${b}not checked${n} (dry run)"
else
compatible="${yes}yes${n} (checked by fwupd)"
fi
bios_disclaimer "WARNING: BIOS UPDATE - ENTIRELY AT YOUR OWN RISK" \ bios_disclaimer "WARNING: BIOS UPDATE - ENTIRELY AT YOUR OWN RISK" \
"Current BIOS: ${b}$current${n}" \ "Current BIOS: ${b}$current${n}" \
"New BIOS: ${b}$BIOS_NEWEST${n}" \ "New BIOS: ${b}$BIOS_NEWEST${n}" \
"Checksum: ${yes}OK${n} (Valve's package)" \ "Checksum: ${yes}OK${n} (Valve's package)" \
"Compatible: $compatible" "Compatible: $compatible"
if ! ask_yn "Do you understand the risks and want to continue?" n; then if ! ask_yn "Do you understand the risks and want to continue?" n; then
info "BIOS update cancelled; nothing was changed."; rm -rf "$tmp"; return 0 info "BIOS update cancelled; nothing was changed."; rm -rf "$BIOS_STAGE_DIR"; return 0
fi fi
bios_disclaimer "LAST CHANCE: THIS FLASHES BIOS $BIOS_NEWEST" \ bios_disclaimer "LAST CHANCE: THIS FLASHES BIOS $BIOS_NEWEST" \
"After this, keep the power on until the machine has fully" \ "After this, keep the power on until the machine has fully" \
@@ -176,28 +224,10 @@ bios_enable() {
local reply local reply
read -rp "$(echo -e "${c_red}${c_bold}Type UPDATE (in capitals) to flash the BIOS, anything else cancels:${c_reset} ")" reply read -rp "$(echo -e "${c_red}${c_bold}Type UPDATE (in capitals) to flash the BIOS, anything else cancels:${c_reset} ")" reply
if [[ "$reply" != UPDATE ]]; then if [[ "$reply" != UPDATE ]]; then
info "BIOS update cancelled; nothing was changed."; rm -rf "$tmp"; return 0 info "BIOS update cancelled; nothing was changed."; rm -rf "$BIOS_STAGE_DIR"; return 0
fi fi
bios_flash
if [[ -n "$BIOS_DRY_RUN" ]]; then
ok "Dry run: would run: fwupdmgr install -y --no-reboot-check $(basename "$BIOS_CAB")"
ok "Dry run finished; nothing was flashed."
# Treated as staged, so the restart choices that follow a real
# update show up too; restarting only prints (see restart_now).
BIOS_NEEDS_RESTART=1
rm -rf "$tmp"
return 0
fi
info "Handing BIOS $BIOS_NEWEST to fwupd. Do NOT turn off the power from now on."
# -y: we already asked twice; --no-reboot-check: the wizard's own
# restart question comes at the end.
if ! sudo fwupdmgr install -y --no-reboot-check "$tmp/$BIOS_CAB"; then
err "fwupd could not install the BIOS update (see above); the BIOS was not changed."
rm -rf "$tmp"
return 1
fi
rm -rf "$tmp"
BIOS_NEEDS_RESTART=1
ok "BIOS $BIOS_NEWEST is staged. It is written during the next restart:"
warn "keep the power on and don't touch the machine until it has fully started again."
} }
# For the app (lib/backend.sh): the flash step alone, after its warnings.
bios_flash_only() { bios_flash; }
+148 -14
View File
@@ -71,9 +71,9 @@ ApplicationWindow {
kpin: { label: "Pin the kernel", hint: "Fixes rebooting after shutdown", kpin: { label: "Pin the kernel", hint: "Fixes rebooting after shutdown",
body: "Newer CachyOS kernels make the Steam Machine reboot instead of shutting down. Untick once CachyOS fixes that.", body: "Newer CachyOS kernels make the Steam Machine reboot instead of shutting down. Untick once CachyOS fixes that.",
changes: ["linux-cachyos from Steamify's release (signature checked)", "Kept in /var/cache/steamify/kernel", "Added to IgnorePkg"] }, changes: ["linux-cachyos from Steamify's release (signature checked)", "Kept in /var/cache/steamify/kernel", "Added to IgnorePkg"] },
bios: { label: "Update BIOS", hint: "Runs in the terminal version (two warnings, typed confirmation)", bios: { label: "Update BIOS", hint: "",
body: "Installs Valve's newest Steam Machine BIOS, at your own risk. The app leaves this to the terminal version, which asks twice and wants UPDATE typed.", 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: ["Run steamify.sh in Konsole for this"] } 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"] }
}) })
// --- State --- // --- State ---
@@ -91,6 +91,20 @@ ApplicationWindow {
property string runError: "" property string runError: ""
property bool wrongPassword: false property bool wrongPassword: false
property int doneCount: 0 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 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 var bios: status.bios || null
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";
}
ListModel { id: logModel } ListModel { id: logModel }
readonly property var status: backend.status || ({}) readonly property var status: backend.status || ({})
@@ -151,15 +165,39 @@ ApplicationWindow {
screen = "applying"; screen = "applying";
backend.apply(wantedIds(), want.gaming ? boot : "", reapply, password || ""); backend.apply(wantedIds(), want.gaming ? boot : "", reapply, password || "");
} }
function askPassword(forWhat) {
pending = forWhat;
if (backend.needsPassword()) { wrongPassword = false; pw.text = ""; screen = "password"; pw.forceActiveFocus(); return true; }
return false;
}
function onApplyPressed() { function onApplyPressed() {
if (plan.length === 0) return; if (plan.length === 0) return;
if (backend.needsPassword()) { wrongPassword = false; pw.text = ""; screen = "password"; pw.forceActiveFocus(); } if (!askPassword("apply")) startApply("");
else startApply("");
} }
function submitPassword() { function submitPassword() {
if (!backend.checkPassword(pw.text)) { wrongPassword = true; pw.selectAll(); return; } if (!backend.checkPassword(pw.text)) { wrongPassword = true; pw.selectAll(); return; }
var p = pw.text; pw.text = ""; var p = pw.text; pw.text = "";
startApply(p); if (pending === "bios") { sessionPassword = p; biosCheck(); }
else startApply(p);
}
// --- BIOS: check -> warning 1 -> warning 2 -> flash ---
function startBios() {
if (!bios || !bios.selectable) return;
biosInfo = {}; sessionPassword = "";
if (!askPassword("bios")) biosCheck();
}
function biosStep(action) {
steps = {}; failed = []; restartNeeded = false; runError = ""; doneCount = 0; logModel.clear();
plan = [{ id: "bios", action: action }];
var s = {}; s.bios = "wait"; steps = s;
biosRun = true; screen = "applying";
}
function biosCheck() { biosStep("check"); backend.biosPrepare(sessionPassword); }
function biosCancel() { sessionPassword = ""; holdProgress = 0; holding = false; biosRun = false; syncFromStatus(); screen = "menu"; }
function biosFlash() { holdProgress = 0; holding = false; biosStep("flash"); backend.biosFlash(sessionPassword); }
Timer {
interval: 50; repeat: true; running: screen === "bios2" && holding
onTriggered: { holdProgress = Math.min(1, holdProgress + 0.01); if (holdProgress >= 1) biosFlash(); }
} }
Connections { Connections {
target: backend target: backend
@@ -168,7 +206,9 @@ ApplicationWindow {
if (ev.event === "start") { s[ev.id] = "run"; steps = s; } if (ev.event === "start") { s[ev.id] = "run"; steps = s; }
else if (ev.event === "done") { s[ev.id] = ev.ok ? "ok" : "fail"; steps = s; doneCount++; } else if (ev.event === "done") { s[ev.id] = ev.ok ? "ok" : "fail"; steps = s; doneCount++; }
else if (ev.event === "log") { logModel.append({ line: ev.line }); if (logModel.count > 400) logModel.remove(0); } else if (ev.event === "log") { logModel.append({ line: ev.line }); if (logModel.count > 400) logModel.remove(0); }
else if (ev.event === "bios-ready") { biosInfo = ev; biosFocus = 0; holdProgress = 0; screen = "bios1"; }
else if (ev.event === "finished") { else if (ev.event === "finished") {
if (!(biosRun && plan.length && plan[0].action === "check" && !ev.failed.length && !ev.error && !ev.nothing)) sessionPassword = "";
failed = ev.failed || []; restartNeeded = !!ev.restart; failed = ev.failed || []; restartNeeded = !!ev.restart;
runError = ev.error === "wrong-password" ? "The password didn't work." : (ev.error ? "sudo isn't available." : ""); runError = ev.error === "wrong-password" ? "The password didn't work." : (ev.error ? "sudo isn't available." : "");
screen = "done"; screen = "done";
@@ -185,6 +225,7 @@ ApplicationWindow {
else if (a === "accept") { else if (a === "accept") {
if (sel === rows.length) { goReview(false); return; } if (sel === rows.length) { goReview(false); return; }
var r = rows[sel]; var r = rows[sel];
if (r.kind === "action" && r.id === "bios") { startBios(); return; }
if (r.kind === "choice") boot = boot === "gamescope" ? "desktop" : "gamescope"; if (r.kind === "choice") boot = boot === "gamescope" ? "desktop" : "gamescope";
else if (r.kind === "toggle") toggle(r.id); else if (r.kind === "toggle") toggle(r.id);
} }
@@ -199,21 +240,36 @@ ApplicationWindow {
} else if (screen === "password") { } else if (screen === "password") {
if (a === "accept" || a === "apply") submitPassword(); if (a === "accept" || a === "apply") submitPassword();
else if (a === "back") { pw.text = ""; screen = "review"; } else if (a === "back") { pw.text = ""; screen = "review"; }
} else if (screen === "bios1") {
if (a === "left") biosFocus = 0;
else if (a === "right") biosFocus = 1;
else if (a === "accept") { if (biosFocus === 1) { holdProgress = 0; biosTyped.text = ""; screen = "bios2"; if (inputType === "keyboard") biosTyped.forceActiveFocus(); } 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") { } else if (screen === "done") {
if (a === "accept" && restartNeeded) backend.restart(); if (a === "accept" && restartNeeded) backend.restart();
else if (a === "back" || a === "accept") { syncFromStatus(); screen = "menu"; } else if (a === "back" || a === "accept") { biosRun = false; syncFromStatus(); screen = "menu"; }
} }
} }
Connections { Connections {
target: gamepad target: gamepad
function onButton(b) { function onButton(b) {
if (b === "a_up") { if (screen === "bios2") act("release"); return; }
if (b.slice(-3) === "_up") return;
inputType = "controller"; inputType = "controller";
if (b === "a" && screen === "bios2") { act("hold"); return; }
var map = { a: "accept", b: "back", x: "apply", y: "reapply", start: "apply", up: "up", down: "down", left: "left", right: "right" }; var map = { a: "accept", b: "back", x: "apply", y: "reapply", start: "apply", up: "up", down: "down", left: "left", right: "right" };
if (map[b]) act(map[b]); if (map[b]) act(map[b]);
} }
} }
function keyAct(e) { function keyAct(e) {
var fromRemote = Date.now() - remoteAt < 400; var fromRemote = Date.now() - remoteAt < 400;
if (screen === "bios2" && inputType !== "keyboard" && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) {
if (!e.isAutoRepeat) act("hold"); e.accepted = true; return;
}
if (!fromRemote) inputType = "keyboard"; if (!fromRemote) inputType = "keyboard";
var k = e.key; var k = e.key;
if (k === Qt.Key_Up) act("up"); if (k === Qt.Key_Up) act("up");
@@ -266,6 +322,18 @@ ApplicationWindow {
Text { id: ct; anchors.centerIn: parent; text: parent.text.toUpperCase(); color: parent.fg; font.family: t.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.6 } Text { id: ct; anchors.centerIn: parent; text: parent.text.toUpperCase(); color: parent.fg; font.family: t.body; font.pixelSize: 12; font.weight: Font.DemiBold; font.letterSpacing: 0.6 }
} }
component RiskList: Column {
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: parent.width
Rectangle { width: 6; height: 6; radius: 3; color: t.bad; y: 9 }
Text { text: parent.modelData; width: parent.width - 16; wrapMode: Text.WordWrap; color: t.soft; font.family: t.body; font.pixelSize: 15; lineHeight: 1.3 } }
}
}
// --- The 1280x720 stage, scaled to the window --- // --- The 1280x720 stage, scaled to the window ---
Item { Item {
id: stage id: stage
@@ -274,6 +342,7 @@ ApplicationWindow {
scale: Math.min(win.width / 1280, win.height / 720) scale: Math.min(win.width / 1280, win.height / 720)
focus: true focus: true
Keys.onPressed: function (e) { if (!(screen === "password" && pw.activeFocus && e.key !== Qt.Key_Escape && e.key !== Qt.Key_Return && e.key !== Qt.Key_Enter)) keyAct(e); } Keys.onPressed: function (e) { if (!(screen === "password" && pw.activeFocus && e.key !== Qt.Key_Escape && e.key !== Qt.Key_Return && e.key !== Qt.Key_Enter)) keyAct(e); }
Keys.onReleased: function (e) { if (screen === "bios2" && !e.isAutoRepeat && (e.key === Qt.Key_Return || e.key === Qt.Key_Enter)) act("release"); }
// Header // Header
Rectangle { Rectangle {
@@ -283,7 +352,7 @@ ApplicationWindow {
anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 14 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 } Image { source: iconUrl; width: 40; height: 40; sourceSize: Qt.size(80, 80); anchors.verticalCenter: parent.verticalCenter }
Text { text: "Steamify"; color: t.text; font.family: t.display; font.pixelSize: 28; font.weight: Font.Bold; anchors.verticalCenter: parent.verticalCenter } Text { text: "Steamify"; color: t.text; font.family: t.display; font.pixelSize: 28; font.weight: Font.Bold; anchors.verticalCenter: parent.verticalCenter }
Text { text: screen === "menu" ? "v" + (status.version || "") : "/ " + ({review: "Review", password: "Password", applying: "Applying", done: "Done"})[screen] Text { text: screen === "menu" ? "v" + (status.version || "") : "/ " + ({review: "Review", password: "Password", applying: biosRun ? "BIOS update" : "Applying", done: "Done", bios1: "BIOS update", bios2: "BIOS update"})[screen]
color: t.faint; font.family: screen === "menu" ? t.mono : t.body; font.pixelSize: screen === "menu" ? 13 : 15; anchors.verticalCenter: parent.verticalCenter } color: t.faint; font.family: screen === "menu" ? t.mono : t.body; font.pixelSize: screen === "menu" ? 13 : 15; anchors.verticalCenter: parent.verticalCenter }
} }
Rectangle { Rectangle {
@@ -356,9 +425,9 @@ ApplicationWindow {
anchors.verticalCenter: parent.verticalCenter; spacing: 2 anchors.verticalCenter: parent.verticalCenter; spacing: 2
x: row.modelData.parent ? 46 : 16 x: row.modelData.parent ? 46 : 16
width: controls.x - x - 16 width: controls.x - x - 16
opacity: row.modelData.kind === "action" ? 0.6 : 1 opacity: row.modelData.kind === "action" && !(bios && bios.selectable) ? 0.6 : 1
Text { text: label(row.modelData); color: t.textHi; font.family: t.body; font.pixelSize: 17; font.weight: Font.DemiBold; elide: Text.ElideRight; width: parent.width } Text { text: label(row.modelData); color: t.textHi; font.family: t.body; font.pixelSize: 17; font.weight: Font.DemiBold; elide: Text.ElideRight; width: parent.width }
Text { text: (texts[row.modelData.id] && texts[row.modelData.id].hint) || row.modelData.hint; color: t.mute; font.family: t.body; font.pixelSize: 13; elide: Text.ElideRight; width: parent.width } Text { text: row.modelData.id === "bios" ? biosHint() : ((texts[row.modelData.id] && texts[row.modelData.id].hint) || row.modelData.hint); color: t.mute; font.family: t.body; font.pixelSize: 13; elide: Text.ElideRight; width: parent.width }
} }
// Right: every control ends on the same edge // Right: every control ends on the same edge
Item { Item {
@@ -384,7 +453,7 @@ ApplicationWindow {
// action // action
Rectangle { visible: row.modelData.kind === "action"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter Rectangle { visible: row.modelData.kind === "action"; anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter
width: at.implicitWidth + 24; height: 30; radius: 8; color: t.warnBg width: at.implicitWidth + 24; height: 30; radius: 8; color: t.warnBg
Text { id: at; anchors.centerIn: parent; text: "In the terminal"; color: t.warn; font.family: t.body; font.pixelSize: 13; font.weight: Font.DemiBold } } Text { id: at; anchors.centerIn: parent; text: bios && bios.selectable ? "Update…" : (bios && bios.newest ? "Up to date" : "Unavailable"); color: t.warn; font.family: t.body; font.pixelSize: 13; font.weight: Font.DemiBold } }
} }
} }
} }
@@ -540,7 +609,7 @@ ApplicationWindow {
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width
Column { Column {
x: 40; y: 32; width: 560; spacing: 18 x: 40; y: 32; width: 560; spacing: 18
Text { text: "Applying your changes"; color: t.text; font.family: t.display; font.pixelSize: 34; font.weight: Font.DemiBold } Text { text: biosRun ? (plan.length && plan[0].action === "check" ? "Checking the BIOS update" : "Installing the BIOS update") : "Applying your changes"; color: t.text; font.family: t.display; font.pixelSize: 34; font.weight: Font.DemiBold }
Rectangle { width: 560; height: 10; radius: 5; color: t.line Rectangle { width: 560; height: 10; radius: 5; color: t.line
Rectangle { height: 10; radius: 5; color: t.accent; width: plan.length ? parent.width * Math.min(1, (doneCount + 0.3) / plan.length) : 0; Behavior on width { NumberAnimation { duration: 300 } } } } Rectangle { height: 10; radius: 5; color: t.accent; width: plan.length ? parent.width * Math.min(1, (doneCount + 0.3) / plan.length) : 0; Behavior on width { NumberAnimation { duration: 300 } } } }
Row { width: 560 Row { width: 560
@@ -556,7 +625,7 @@ ApplicationWindow {
border.width: parent.parent.st === "wait" || parent.parent.st === "run" ? 2 : 0; border.color: parent.parent.st === "run" ? t.accent : "#343f50" border.width: parent.parent.st === "wait" || parent.parent.st === "run" ? 2 : 0; border.color: parent.parent.st === "run" ? t.accent : "#343f50"
Text { anchors.centerIn: parent; text: parent.parent.parent.st === "ok" ? "✓" : (parent.parent.parent.st === "fail" ? "!" : ""); color: parent.parent.parent.st === "ok" ? t.good : t.bad; font.pixelSize: 13; font.weight: Font.Bold } Text { anchors.centerIn: parent; text: parent.parent.parent.st === "ok" ? "✓" : (parent.parent.parent.st === "fail" ? "!" : ""); color: parent.parent.parent.st === "ok" ? t.good : t.bad; font.pixelSize: 13; font.weight: Font.Bold }
RotationAnimator on rotation { running: parent.parent.parent.st === "run"; from: 0; to: 360; duration: 1000; loops: Animation.Infinite } } RotationAnimator on rotation { running: parent.parent.parent.st === "run"; from: 0; to: 360; duration: 1000; loops: Animation.Infinite } }
Text { text: ({ on: "Turn on ", off: "Turn off ", again: "Re-apply ", desktop: "Boot into ", gaming: "Boot into " })[parent.parent.modelData.action] + ((texts[parent.parent.modelData.id] || {}).label || parent.parent.modelData.id) Text { text: ({ on: "Turn on ", off: "Turn off ", again: "Re-apply ", desktop: "Boot into ", gaming: "Boot into ", check: "Download and check the ", flash: "Hand to fwupd: the " })[parent.parent.modelData.action] + ((texts[parent.parent.modelData.id] || {}).label || parent.parent.modelData.id)
color: parent.parent.st === "wait" ? t.faint : t.textHi; font.family: t.body; font.pixelSize: 16; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter; width: 380; elide: Text.ElideRight } color: parent.parent.st === "wait" ? t.faint : t.textHi; font.family: t.body; font.pixelSize: 16; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter; width: 380; elide: Text.ElideRight }
Text { text: ({ wait: "Waiting", run: "Working…", ok: "Done", fail: "Problem" })[parent.parent.st]; color: parent.parent.st === "ok" ? t.good : (parent.parent.st === "fail" ? t.bad : "#b8c3d1"); font.family: t.body; font.pixelSize: 13; anchors.verticalCenter: parent.verticalCenter } Text { text: ({ wait: "Waiting", run: "Working…", ok: "Done", fail: "Problem" })[parent.parent.st]; color: parent.parent.st === "ok" ? t.good : (parent.parent.st === "fail" ? t.bad : "#b8c3d1"); font.family: t.body; font.pixelSize: 13; anchors.verticalCenter: parent.verticalCenter }
} }
@@ -573,6 +642,71 @@ ApplicationWindow {
} }
} }
// ================= BIOS: warning 1 =================
Item {
visible: screen === "bios1"
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width
Rectangle {
x: 40; y: 28; width: 1200; height: parent.height - 28 - 64 - 24; radius: 16; color: "#1d1416"; border.width: 2; border.color: "#b33a3a"
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: t.bad; font.pixelSize: 26; font.weight: Font.Bold } }
Text { text: "BIOS update – entirely at your own risk"; color: "#ffd9d3"; font.family: t.display; font.pixelSize: 30; font.weight: Font.DemiBold; anchors.verticalCenter: parent.verticalCenter } }
RiskList { width: 640 }
}
Rectangle { width: 440; height: 250; radius: 14; color: "#161d27"
Column { anchors.fill: parent; anchors.margins: 22; spacing: 12
Repeater { model: [["Current BIOS", biosInfo.current || ""], ["New BIOS", biosInfo.newest || ""], ["Checksum", "OK (Valve's package)"],
["Compatible", biosInfo.compatible === "yes" ? "Yes (checked by fwupd)" : "Not checked (dry run)"]]
Row { required property var modelData; width: 396
Text { text: parent.modelData[0]; color: t.faint; font.family: t.body; font.pixelSize: 15; width: 150 }
Text { text: parent.modelData[1]; color: parent.modelData[1].indexOf("Not") === 0 ? t.warn : t.text; font.family: t.mono; font.pixelSize: 15; width: 246; horizontalAlignment: Text.AlignRight; elide: Text.ElideLeft } } }
} }
}
}
Rectangle {
anchors.bottom: parent.bottom; width: parent.width; height: 64; color: t.bar
Rectangle { width: parent.width; height: 1; color: t.line }
Text { anchors.left: parent.left; anchors.leftMargin: 40; anchors.verticalCenter: parent.verticalCenter; text: "Do you understand the risks and want to continue?"; color: t.soft; font.family: t.body; font.pixelSize: 15 }
Row { anchors.right: parent.right; anchors.rightMargin: 40; anchors.verticalCenter: parent.verticalCenter; spacing: 12
Btn { k: biosFocus === 0 ? g.ok : "◀"; text: "Cancel"; focusRing: biosFocus === 0; onClicked: biosCancel() }
Btn { k: biosFocus === 1 ? g.ok : "▶"; text: "I understand, continue"; focusRing: biosFocus === 1; color: "#8a2c2c"
onClicked: { biosFocus = 1; act("accept"); } } }
}
}
// ================= BIOS: warning 2 (last chance) =================
Item {
visible: screen === "bios2"
anchors.top: header.bottom; anchors.bottom: parent.bottom; width: parent.width
Rectangle {
anchors.centerIn: parent; width: 760; height: b2.implicitHeight + 64; radius: 20; color: "#1d1416"; border.width: 2; border.color: "#b33a3a"
Column { id: b2; anchors.fill: parent; anchors.margins: 32; spacing: 18
Text { text: "Last chance: this flashes BIOS " + (biosInfo.newest || ""); color: "#ffd9d3"; font.family: t.display; font.pixelSize: 32; font.weight: Font.DemiBold }
Text { width: parent.width; wrapMode: Text.WordWrap; color: t.soft; font.family: t.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." }
// keyboard: type UPDATE
Column { visible: inputType === "keyboard"; width: parent.width; spacing: 8
Text { text: "Type UPDATE (in capitals) and press Enter to flash the BIOS:"; color: t.text; font.family: t.body; font.pixelSize: 15; font.weight: Font.DemiBold }
TextField { id: biosTyped; width: 320; height: 50; color: t.textHi; font.family: t.mono; font.pixelSize: 20; leftPadding: 14
placeholderText: "UPDATE"; placeholderTextColor: "#6b5a5a"
background: Rectangle { radius: 10; color: t.bg; border.width: 2; border.color: biosTyped.text === "UPDATE" ? t.bad : "#5a3a3a" }
Keys.onReturnPressed: if (text === "UPDATE") biosFlash()
Keys.onEnterPressed: if (text === "UPDATE") biosFlash()
Keys.onEscapePressed: biosCancel() } }
// controller / remote: hold A or OK
Column { visible: inputType !== "keyboard"; width: parent.width; spacing: 10
Text { text: "Hold " + g.ok + " for 5 seconds to flash the BIOS. Let go to stop."; color: t.text; font.family: t.body; font.pixelSize: 15; font.weight: Font.DemiBold }
Rectangle { width: parent.width; height: 14; radius: 7; color: "#3a2224"
Rectangle { height: 14; radius: 7; color: t.bad; width: parent.width * holdProgress } } }
Row { spacing: 12
Btn { k: g.back; text: "Cancel"; onClicked: biosCancel() }
Text { visible: bios && bios.dryRun; text: "Dry run: nothing will be flashed."; color: t.warn; font.family: t.body; font.pixelSize: 14; anchors.verticalCenter: parent.verticalCenter } }
}
}
}
// ================= DONE ================= // ================= DONE =================
Item { Item {
visible: screen === "done" visible: screen === "done"
@@ -584,7 +718,7 @@ ApplicationWindow {
Text { anchors.centerIn: parent; text: failed.length || runError ? "!" : "✓"; color: failed.length || runError ? t.warn : t.good; font.pixelSize: 36; font.weight: Font.Bold } } Text { anchors.centerIn: parent; text: failed.length || runError ? "!" : "✓"; color: failed.length || runError ? t.warn : t.good; font.pixelSize: 36; font.weight: Font.Bold } }
Text { text: runError ? "Nothing changed" : (failed.length ? "Done, with a problem" : "All done"); color: t.text; font.family: t.display; font.pixelSize: 40; font.weight: Font.DemiBold; anchors.horizontalCenter: parent.horizontalCenter } Text { text: runError ? "Nothing changed" : (failed.length ? "Done, with a problem" : "All done"); color: t.text; font.family: t.display; font.pixelSize: 40; font.weight: Font.DemiBold; anchors.horizontalCenter: parent.horizontalCenter }
Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap; color: t.soft; font.family: t.body; font.pixelSize: 17 Text { width: parent.width; horizontalAlignment: Text.AlignHCenter; wrapMode: Text.WordWrap; color: t.soft; font.family: t.body; font.pixelSize: 17
text: runError ? runError : (plan.length + (plan.length === 1 ? " change" : " changes") + " applied." + (restartNeeded ? " Some take effect after a restart." : "")) } text: runError ? runError : (biosRun ? (failed.length ? "The BIOS was not changed." : (bios && bios.dryRun ? "Dry run: nothing was flashed. " : "BIOS " + (biosInfo.newest || "") + " is staged and is written during the restart. ") + (failed.length ? "" : "Keep the power on and don't touch the machine until it has fully started again, even if the screen stays black.")) : (plan.length + (plan.length === 1 ? " change" : " changes") + " applied." + (restartNeeded ? " Some take effect after a restart." : ""))) }
Rectangle { visible: failed.length > 0; anchors.horizontalCenter: parent.horizontalCenter; width: ft.implicitWidth + 32; height: 44; radius: 12; color: t.warnBg Rectangle { visible: failed.length > 0; anchors.horizontalCenter: parent.horizontalCenter; width: ft.implicitWidth + 32; height: 44; radius: 12; color: t.warnBg
Text { id: ft; anchors.centerIn: parent; color: "#f2c27a"; font.family: t.body; font.pixelSize: 14 Text { id: ft; anchors.centerIn: parent; color: "#f2c27a"; font.family: t.body; font.pixelSize: 14
text: "Had a problem: " + failed.map(function (id) { return (texts[id] || {}).label || id; }).join(", ") + ". See the details." } } text: "Had a problem: " + failed.map(function (id) { return (texts[id] || {}).label || id; }).join(", ") + ". See the details." } }
+27 -12
View File
@@ -81,7 +81,8 @@ def controller_devices():
class Gamepad(QObject): class Gamepad(QObject):
"""Emits button("a"|"b"|"x"|"y"|"up"|...) from any controller.""" """Emits button("a"|"b"|"x"|"y"|"up"|...) from any controller, and
"<name>_up" when a button is released."""
button = Signal(str) button = Signal(str)
def __init__(self): def __init__(self):
@@ -119,8 +120,10 @@ class Gamepad(QObject):
continue continue
for off in range(0, len(data) - EVENT.size + 1, EVENT.size): for off in range(0, len(data) - EVENT.size + 1, EVENT.size):
_, _, typ, code, value = EVENT.unpack_from(data, off) _, _, typ, code, value = EVENT.unpack_from(data, off)
if typ == EV_KEY and value == 1 and code in BUTTONS: if typ == EV_KEY and code in BUTTONS and value in (0, 1):
self.button.emit(BUTTONS[code]) # Presses as "a"; releases as "a_up" (the BIOS
# confirmation is a 5-second hold).
self.button.emit(BUTTONS[code] + ("" if value else "_up"))
elif typ == EV_ABS and code in (ABS_HAT0X, ABS_HAT0Y) and value: elif typ == EV_ABS and code in (ABS_HAT0X, ABS_HAT0Y) and value:
self.button.emit({(ABS_HAT0X, -1): "left", (ABS_HAT0X, 1): "right", self.button.emit({(ABS_HAT0X, -1): "left", (ABS_HAT0X, 1): "right",
(ABS_HAT0Y, -1): "up", (ABS_HAT0Y, 1): "down"}[(code, value)]) (ABS_HAT0Y, -1): "up", (ABS_HAT0Y, 1): "down"}[(code, value)])
@@ -221,16 +224,9 @@ class Backend(QObject):
shutil.rmtree(self._secret_dir, ignore_errors=True) shutil.rmtree(self._secret_dir, ignore_errors=True)
self._secret_dir = None self._secret_dir = None
@Slot("QVariant", str, bool, str) def _start(self, args, password):
def apply(self, ids, boot, reapply, password):
if self._busy: if self._busy:
return return
args = [self._script, "--backend", "apply"]
if reapply:
args.append("--reapply")
if boot:
args += ["--boot", boot]
args += list(ids)
env = QProcessEnvironment.systemEnvironment() env = QProcessEnvironment.systemEnvironment()
if password: if password:
self._secret_dir, askpass, bindir = self._make_askpass(password) self._secret_dir, askpass, bindir = self._make_askpass(password)
@@ -240,7 +236,7 @@ class Backend(QObject):
p.setProcessEnvironment(env) p.setProcessEnvironment(env)
p.setProcessChannelMode(QProcess.MergedChannels) p.setProcessChannelMode(QProcess.MergedChannels)
p.setProgram("bash") p.setProgram("bash")
p.setArguments(args) p.setArguments([self._script, "--backend"] + args)
self._buf = b"" self._buf = b""
p.readyReadStandardOutput.connect(lambda: self._read(p)) p.readyReadStandardOutput.connect(lambda: self._read(p))
p.finished.connect(lambda *_: self._finished(p)) p.finished.connect(lambda *_: self._finished(p))
@@ -249,6 +245,25 @@ class Backend(QObject):
self.busyChanged.emit() self.busyChanged.emit()
p.start() p.start()
@Slot("QVariant", str, bool, str)
def apply(self, ids, boot, reapply, password):
args = ["apply"]
if reapply:
args.append("--reapply")
if boot:
args += ["--boot", boot]
self._start(args + list(ids), password)
@Slot(str)
def biosPrepare(self, password):
"""Download, checksum, fwupd check: ends with a bios-ready event."""
self._start(["bios-prepare"], password)
@Slot(str)
def biosFlash(self, password):
"""Only after both warnings in the app."""
self._start(["bios-flash"], password)
def _read(self, p): def _read(self, p):
self._buf += bytes(p.readAllStandardOutput()) self._buf += bytes(p.readAllStandardOutput())
while b"\n" in self._buf: while b"\n" in self._buf: