GNOME's Multitasking panel asked one workspace question worth reproducing, and it is not which workspace goes on which screen. It is whether the second screen participates at all: workspaces on the primary display only, or each screen with its own. Ten rows of per-workspace assignment would be more powerful and worse. Off is Hyprland's own behaviour and emits nothing. On pins workspaces 1 to 10 -- however many ALT+1..ALT+0 actually reach, read from keybinds.lua rather than written down twice -- to whichever output is recorded as primary. With no primary recorded, nothing is pinned: guessing one would move every workspace onto whichever output happened to sort first, and this machine is in exactly that state. Applying is a reload, which is the part that shaped the design. Hyprland reads workspace rules at config time and will not remove one afterwards -- a rule written with an empty monitor keeps its old binding, which was checked rather than assumed. Only a reload clears them, so the config is the only honest source and the page cannot pretend a change has landed before one happens. Hence a service that reads `hyprctl workspacerules` back rather than inferring success from having written the preference, and a Reload row that exists only while the two disagree. Verified end to end against the live compositor and put back: off emits nothing, on emits ten rules naming the primary, and turning it off clears them. The settings file came back byte-identical. Claude-Session: https://claude.ai/code/session_01Q84axqUE5inJhf5Jz9CFy1
121 lines
4.6 KiB
QML
121 lines
4.6 KiB
QML
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()
|
|
}
|