import QtQuick import Quickshell import Quickshell.Io import qs.Common Item { id: service property var daemon: null readonly property var pluginData: (daemon && daemon.pluginData) ? daemon.pluginData : ({}) readonly property string saveDirectory: String(pluginData.captureDirectory || "~/Videos/GameBar").trim() readonly property int fps: Math.max(10, pluginData.captureFps || 60) readonly property int replaySeconds: Math.max(5, pluginData.captureReplaySeconds || 30) readonly property string audioSource: String(pluginData.captureAudio || "default_output").trim() readonly property string quality: String(pluginData.captureQuality || "very_high").trim() // "" | "recording" | "replay" property string mode: "" property string targetMonitor: "" property string lastFile: "" property string lastError: "" property double startedAt: 0 property int elapsedSeconds: 0 readonly property bool recording: mode === "recording" readonly property bool replayArmed: mode === "replay" readonly property bool busy: mode !== "" signal notice(string message) function expandedDirectory() { const home = Quickshell.env("HOME") || ""; if (service.saveDirectory.startsWith("~/")) return home + service.saveDirectory.substring(1); return service.saveDirectory; } function timestampedFile() { const now = new Date(); const stamp = Qt.formatDateTime(now, "yyyy-MM-dd_HH-mm-ss"); return service.expandedDirectory() + "/GameBar_" + stamp + ".mp4"; } function baseArgs(monitor) { return ["-w", monitor, "-f", String(service.fps), "-a", service.audioSource, "-q", service.quality, "-cursor", "no"]; } function startRecording(monitor) { if (service.busy) return; service.targetMonitor = monitor || "focused"; service.lastFile = service.timestampedFile(); service.lastError = ""; recorder.command = ["sh", "-c", 'mkdir -p "$1" && exec gpu-screen-recorder "${@:2}"', "gameBar", service.expandedDirectory()].concat(service.baseArgs(service.targetMonitor)).concat(["-o", service.lastFile]); recorder.running = true; service.mode = "recording"; service.startedAt = Date.now(); service.elapsedSeconds = 0; service.notice("Recording started"); } function startReplay(monitor) { if (service.busy) return; service.targetMonitor = monitor || "focused"; service.lastError = ""; recorder.command = ["sh", "-c", 'mkdir -p "$1" && exec gpu-screen-recorder "${@:2}"', "gameBar", service.expandedDirectory()].concat(service.baseArgs(service.targetMonitor)).concat(["-c", "mp4", "-r", String(service.replaySeconds), "-o", service.expandedDirectory()]); recorder.running = true; service.mode = "replay"; service.startedAt = Date.now(); service.elapsedSeconds = 0; service.notice("Replay buffer armed (" + service.replaySeconds + "s)"); } // SIGINT stops and saves a recording; in replay mode it just stops. function stop() { if (!service.busy) return; recorder.signal(2); service.notice(service.recording ? "Recording saved" : "Replay buffer stopped"); } // SIGUSR1 writes the last to disk, replay mode only. function saveReplay() { if (!service.replayArmed) return; recorder.signal(10); service.notice("Replay saved"); } function toggleRecording(monitor) { if (service.recording) service.stop(); else if (!service.busy) service.startRecording(monitor); } function toggleReplay(monitor) { if (service.replayArmed) service.stop(); else if (!service.busy) service.startReplay(monitor); } function screenshot(mode) { // Delegate to the quickCapture plugin when it is installed. Quickshell.execDetached(["dms", "ipc", "call", "quickCapture", "screenshot", mode || "region", "edit"]); } Process { id: recorder running: false stderr: StdioCollector { onStreamFinished: { const raw = String(text || "").trim(); if (raw !== "") service.lastError = raw.split("\n").pop(); } } onExited: exitCode => { service.mode = ""; service.elapsedSeconds = 0; // A clean exit still prints encoder chatter on stderr; only keep it // around when the recorder actually failed. if (exitCode === 0 || exitCode === 130) { service.lastError = ""; return; } if (service.lastError !== "") service.notice("Recorder: " + service.lastError); } } Timer { running: service.busy interval: 1000 repeat: true onTriggered: service.elapsedSeconds = Math.floor((Date.now() - service.startedAt) / 1000) } }