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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user