Files
steamify-cachyos/ui/qml/AppState.qml
T
theupriserandClaude Sonnet 5.5 f3cd053ea0
Sync git.upriser.nl mirror / sync (push) Skipped
feat: Gaming on NVIDIA is not offered any more, the SteamOS conversion covers NVIDIA PCs
The conversion installs the patched gamescope on every NVIDIA PC and sets up the kernel parameters, suspend memory and 32-bit libraries on a supported card; it turns off an existing Gaming on NVIDIA.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkpbsMw4rQyBsxDG6ebHK2
2026-10-13 18:27:13 +02:00

293 lines
15 KiB
QML

// 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
// Review & apply only when something would change; "Re-apply what's on"
// is for when nothing did.
readonly property bool canApply: computePlan().length > 0
onCanApplyChanged: if (!canApply && sel === rows.length) sel = Math.max(0, rows.length - 1)
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 || []
// Hidden: Big Picture excludes a silent start.
function hidden(i) { return i.id === "nvsilent" && want.bigpicture; }
readonly property var rows: items.filter(function (i) { return (!i.parent || want[i.parent]) && !hidden(i); })
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).
var hasGaming = items.some(function (i) { return i.id === "gaming"; });
if (id === "gaming" && !w.gaming) { w.single = false; w.silent = false; w.glyphs = false; }
if (id === "single" && w.single && hasGaming) w.gaming = true;
if (id === "nvidia") w.bigpicture = w.nvidia;
if (id === "bigpicture" && w.bigpicture) { w.nvidia = true; w.nvsilent = false; }
if (id === "nvsilent" && w.nvsilent) { w.nvidia = true; w.bigpicture = false; }
if (id === "nvidia" && !w.nvidia) w.nvsilent = false;
if (id === "silent" && w.silent) w.gaming = true;
if (id === "glyphs" && w.glyphs) 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;
if (id === "launcher" && items.some(function (i) { return i.id === "steamgame"; })) w.steamgame = w.launcher;
if (id === "steamgame" && w.steamgame) w.launcher = 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 (hidden(it)) { if (it.on) p.push({ id: it.id, action: "off" }); 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) {
if (!again && !canApply) return;
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] && !hidden(items[i]) && (!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);
// The greyed-out Apply button can't be selected.
else if (a === "down") sel = Math.min(canApply ? rows.length : rows.length - 1, 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);
// B/Esc/Back don't quit: Steam's desktop layout sends Esc for a
// Steam Controller's B, which should only ever go back.
else if (a === "quit") 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 = Input.fromSteam ? "steam" : "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");
// Enter selects, like A on a controller: Steam's desktop layout sends
// Enter for a Steam Controller's A (and Space for Y). Ctrl+Enter
// goes straight to Review & apply.
else if ((k === Qt.Key_Return || k === Qt.Key_Enter) && (e.modifiers & Qt.ControlModifier)) act("apply");
else if (k === Qt.Key_Return || k === Qt.Key_Enter) act("accept");
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 if (k === Qt.Key_Q && (e.modifiers & Qt.ControlModifier)) act("quit");
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");
}
}