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() } }