pragma Singleton // ───────────────────────────────────────────────────────────────────────────── // Whether the other screens join in on workspaces. // // The preference is ordinary; applying it is not. Workspace rules are read by // Hyprland at config time and cannot be taken back at runtime -- writing a rule // with an empty monitor leaves the old binding in place, which was checked // rather than assumed. Only `hyprctl reload` clears them, and it re-runs the // whole config, so monitors.lua re-emits exactly the set the preference asks // for. // // That makes this service two things: the reload, and an honest answer to "has // it actually taken effect yet". The second matters more. A page that offers a // switch and silently does nothing until the next login is the failure this // repository keeps refusing to ship, so `applied` is read back from the // compositor rather than inferred from the preference having been written. // ───────────────────────────────────────────────────────────────────────────── import Quickshell import Quickshell.Io import QtQuick import qs.config Singleton { id: root // The stored intent. readonly property bool primaryOnly: DesktopPreferences.get("workspacesOnPrimaryOnly") === true // What the compositor is actually running, as reported by hyprctl. property var rules: [] property bool reloading: false property string lastError: "" // The keybinds reach ALT+1..ALT+0, and monitors.lua pins that many. readonly property int boundWorkspaces: 10 // Every monitor named by a rule. When workspaces are pinned there is // exactly one, and it is the primary. readonly property var pinnedMonitors: { const names = []; for (const rule of root.rules) { const monitor = rule?.monitor; if (typeof monitor === "string" && monitor !== "" && !names.includes(monitor)) names.push(monitor); } return names; } // Has the compositor caught up with the preference? // // Pinned means every bound workspace carries a rule naming one monitor; // unpinned means no rules at all. Anything in between is a config that was // changed without a reload, which is precisely the state worth reporting. readonly property bool applied: root.primaryOnly ? (root.rules.length >= root.boundWorkspaces && root.pinnedMonitors.length === 1) : root.rules.length === 0 Process { id: query command: ["hyprctl", "-j", "workspacerules"] stdout: StdioCollector { onStreamFinished: { try { const parsed = JSON.parse(this.text); root.rules = Array.isArray(parsed) ? parsed : []; } catch (error) { // An unparseable answer is not an empty rule set. Claiming // it was would report "not pinned" for a desktop that is. root.lastError = "Could not read the compositor's workspace rules."; } } } } Process { id: reloadRun command: ["hyprctl", "reload"] onExited: (exitCode, exitStatus) => { root.reloading = false; if (exitCode !== 0) { root.lastError = "The compositor did not reload."; return; } root.lastError = ""; // The preference file is written on a timer and the reload has to // re-read it, so the rules are only worth re-reading once both have // had a moment. settle.restart(); } } Timer { id: settle interval: 350 onTriggered: root.refresh() } function refresh(): void { if (!query.running) query.running = true; } // Write the choice, then make it true. Split from the write on purpose: the // page confirms between the two, because a reload is felt across the whole // session rather than in one card. function choose(primaryOnly: bool): void { DesktopPreferences.set("workspacesOnPrimaryOnly", primaryOnly === true); } function apply(): void { if (reloadRun.running) return; root.reloading = true; reloadRun.running = true; } Component.onCompleted: root.refresh() }