pragma Singleton import Quickshell import Quickshell.Io import QtQuick 2.15 // Receives low-rate frame notifications from niri-window-castd. The pixel plane is exposed // through atomically replaced PNG snapshots for QML; native consumers should use the daemon's // memfd returned by the control socket instead. Singleton { id: screencast readonly property string runtimeDir: { const x = Quickshell.env("XDG_RUNTIME_DIR") return (x && x.length) ? x : "/run/user/" + Quickshell.env("UID") } readonly property string socketPath: runtimeDir + "/niri-window-castd.sock" readonly property string snapshotDir: runtimeDir + "/niri-window-castd" property var sequences: ({}) property int revision: 0 function sequenceFor(id) { // Reading revision makes bindings that call this function refresh on every frame event. revision return sequences[id] ?? 0 } function imageSource(id) { const sequence = sequenceFor(id) return sequence > 0 ? "file://" + snapshotDir + "/window-" + id + ".png?seq=" + sequence : "" } function handleLine(line) { if (!line || !line.length) return try { const message = JSON.parse(line) if (message.event === "frame") { const next = Object.assign({}, sequences) next[message.id] = message.seq sequences = next revision++ } else if (message.event === "window-removed") { const next = Object.assign({}, sequences) delete next[message.id] sequences = next revision++ } else if (message.windows) { const next = Object.assign({}, sequences) message.windows.forEach(function(window) { if (window.sequence > 0) next[window.id] = window.sequence }) sequences = next revision++ } } catch (error) { // The daemon's protocol is line-delimited JSON; malformed notifications are ignored // so one bad client message cannot interrupt the bar's own niri event stream. } } Socket { id: socket path: screencast.socketPath connected: false parser: SplitParser { splitMarker: "\n" onRead: function(line) { screencast.handleLine(line) } } onConnectionStateChanged: { if (socket.connected) { socket.write('{"op":"list"}\n') socket.write('{"op":"subscribe"}\n') socket.flush() } else { reconnectTimer.restart() } } } Timer { id: connectTimer interval: 0 repeat: false running: true onTriggered: socket.connected = true } Timer { id: reconnectTimer interval: 2000 repeat: false onTriggered: socket.connected = true } }