Add a Gaming page, and let the desktop react to games

Live first, because unlike every other page here this one has a live
dimension: card temperature, power draw, whether Game Mode actually
engaged. It polls only while it is open, since a settings page nobody is
looking at has no business waking the CPU.

The part that makes it Panama's page rather than a gamemode config
editor is the hook. gamemode runs a script when a game asks for it and
another when the game exits, so the power profile switches to
performance and notifications go quiet for exactly the duration of a
game -- and afterwards both go back to what they WERE, not to a default.
A Do Not Disturb someone set by hand survives a game; a power profile
someone chose is restored rather than replaced. Verified against real
gamemode activation, not merely by calling the hook.

Two things the page reports rather than hides. Game Mode's headline
trick is switching the CPU governor to performance, and this machine
already runs performance, so it says so instead of implying it helps.
And Proton builds are listed but never chosen: Steam picks the runtime
per game, and a control here would claim an authority this page does not
have.

The hook first called a notifications function that did not exist, and
the one that did was a TOGGLE -- the wrong primitive entirely, since
toggling at game start would unsilence notifications that were already
silent. The shell gained an explicit setter and reader.

search-routing-contract kept its own hand-written list of every page,
which made adding one fail as "not a known page" -- a sixth place to
register a page and a sixth chance to forget. It now derives the mapping
from the shell, which already knows it.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-19 18:55:10 -04:00
parent edc504af2e
commit 4cbe3b882a
15 changed files with 915 additions and 26 deletions
+129
View File
@@ -0,0 +1,129 @@
pragma Singleton
// Gaming: what the machine is doing, and what it should do while you play.
//
// The reporting half is cheap -- GPU sensors come from sysfs, gamemode from its
// own daemon -- so this can poll while its page is open. It only polls then:
// a settings page nobody is looking at has no business waking the CPU twice a
// second.
//
// The acting half is not in this file at all. gamemode runs a script when a
// game starts and another when it exits, and that script reads the preferences
// directly, because the shell may have been restarted since the game launched.
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-gaming"
property var gameMode: ({})
property var gpus: []
property var overlay: ({})
property var library: ({})
property bool scanned: false
property string lastError: ""
// Set by the page while it is visible. Nothing polls otherwise.
property bool watching: false
readonly property bool busy: query.running || mutation.running
readonly property bool active: root.gameMode?.active === true
readonly property var primaryGpu: {
for (const gpu of root.gpus) {
if (gpu.discrete)
return gpu;
}
return root.gpus.length > 0 ? root.gpus[0] : null;
}
// The honest version: gamemode's headline trick is switching the governor,
// and it does nothing if the machine already runs that governor.
readonly property bool governorAlreadyThere:
String(root.gameMode?.governorNow ?? "") !== ""
&& root.gameMode?.governorNow === root.gameMode?.governorWhileGaming
function formatBytes(bytes: real): string {
if (!(bytes > 0))
return "0 GB";
return (bytes / 1073741824).toFixed(bytes < 10737418240 ? 1 : 0) + " GB";
}
function gpuSummary(gpu: var): string {
if (!gpu)
return "";
const parts = [];
if (gpu.temperatureC !== null && gpu.temperatureC !== undefined)
parts.push(gpu.temperatureC + " °C");
if (gpu.watts !== null && gpu.watts !== undefined && gpu.watts > 0)
parts.push(gpu.watts + " W");
if (Number(gpu.vramTotalBytes ?? 0) > 0)
parts.push(root.formatBytes(gpu.vramUsedBytes) + " / "
+ root.formatBytes(gpu.vramTotalBytes));
return parts.join(" · ");
}
function refresh(): void {
if (query.running)
return;
query.command = [root.helperPath, "snapshot"];
query.running = true;
}
function absorb(text: string): void {
try {
const parsed = JSON.parse(text);
root.gameMode = parsed.gameMode ?? ({});
root.gpus = Array.isArray(parsed.gpus) ? parsed.gpus : [];
root.overlay = parsed.overlay ?? ({});
root.library = parsed.library ?? ({});
root.lastError = String(parsed.error ?? "");
} catch (error) {
root.lastError = "Could not read the gaming helper's answer.";
console.warn("Gaming: could not parse helper output:", error);
}
root.scanned = true;
}
function run(arguments: var): void {
if (mutation.running)
return;
root.lastError = "";
mutation.command = [root.helperPath].concat(arguments);
mutation.running = true;
}
function setOverlay(enabled: bool): void { root.run(["set-overlay", enabled ? "true" : "false"]); }
function setOverlayPreset(preset: string): void { root.run(["set-overlay-preset", preset]); }
function installHooks(): void { root.run(["install-hooks"]); }
function removeHooks(): void { root.run(["remove-hooks"]); }
Process {
id: query
stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
stderr: StdioCollector {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
}
Process {
id: mutation
stdout: StdioCollector { onStreamFinished: root.absorb(this.text) }
stderr: StdioCollector {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
}
// Three seconds: fast enough that a temperature reading feels live, slow
// enough that it is not a background task of its own.
Timer {
running: root.watching
interval: 3000
repeat: true
onTriggered: root.refresh()
}
}
@@ -51,7 +51,8 @@ Singleton {
"notices": "desktop",
"weather": "home",
"notifications": "notifications",
"capture": "screen-intelligence"
"capture": "screen-intelligence",
"gaming": "gaming"
})
// Settings that are real but have no schema entry, because the system owns
@@ -78,6 +79,10 @@ Singleton {
{ label: "Remote desktop", detail: "See and control this desktop from elsewhere", page: "sharing" },
{ label: "Network name", detail: "The name other machines see", page: "sharing" },
{ label: "File sharing", detail: "Share folders on the network", page: "sharing" },
{ label: "Game Mode", detail: "What happens while a game is running", page: "gaming" },
{ label: "Performance overlay", detail: "Frame rate and sensors on top of the game", page: "gaming" },
{ label: "Proton", detail: "Compatibility tools available to Steam", page: "gaming" },
{ label: "Graphics card", detail: "Temperature, power draw, and video memory", page: "gaming" },
{ label: "Software update", detail: "Packages, applications, and firmware", page: "updates" },
{ label: "Updates", detail: "What is waiting to be installed", page: "updates" },
{ label: "Firmware", detail: "Updates for the hardware itself", page: "updates" },
@@ -92,7 +92,7 @@ Singleton {
}
function openSettings(page: string): void {
const allowed = ["home", "appearance", "displays", "connectivity", "home-phone", "desktop", "sound", "notifications", "screen-intelligence", "shortcuts", "mouse", "privacy", "region", "accounts", "accessibility", "power", "datetime", "applications", "updates", "storage", "snapshots", "users", "sharing", "printers", "services", "about"];
const allowed = ["home", "appearance", "displays", "connectivity", "home-phone", "desktop", "sound", "gaming", "notifications", "screen-intelligence", "shortcuts", "mouse", "privacy", "region", "accounts", "accessibility", "power", "datetime", "applications", "updates", "storage", "snapshots", "users", "sharing", "printers", "services", "about"];
root.settingsPage = allowed.indexOf(page) >= 0 ? page : "home";
DesktopPreferences.set("lastPage", root.settingsPage);
root.settingsOpen = true;