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
126 lines
5.5 KiB
Bash
Executable File
126 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Workspaces on the primary display only.
|
|
#
|
|
# monitors.lua turns one preference into workspace rules, and the rules are the
|
|
# part that cannot be taken back: Hyprland reads them at config time and offers
|
|
# no way to remove one afterwards -- an empty monitor leaves the old binding in
|
|
# place, which was checked rather than assumed. Only a reload clears them, so
|
|
# what this file emits IS the state of the desktop, and emitting one rule too
|
|
# many strands a workspace on a screen until the next reload.
|
|
#
|
|
# The Lua is exercised with a stubbed `hl`, so the rules can be counted without
|
|
# a compositor and without touching the running desktop.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
hypr_dir="$repo_dir/config/dot/hypr"
|
|
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
|
|
page="$repo_dir/config/dot/quickshell/modules/settings/DisplaysPage.qml"
|
|
service="$repo_dir/config/dot/quickshell/services/Workspaces.qml"
|
|
keybinds="$hypr_dir/keybinds.lua"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
command -v lua >/dev/null 2>&1 || {
|
|
printf 'workspace rules contract: lua is not installed\n' >&2
|
|
exit 1
|
|
}
|
|
|
|
work="$(mktemp -d /tmp/panama-workspace-rules.XXXXXX)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
mkdir -p "$work/config/panama"
|
|
|
|
# Every workspace rule monitors.lua emits for a given settings file, as
|
|
# "workspace monitor" lines. hl.monitor is swallowed: this is about workspaces,
|
|
# and a monitor call is not one.
|
|
emit() {
|
|
printf '%s' "$1" >"$work/config/panama/settings.json"
|
|
XDG_CONFIG_HOME="$work/config" lua -e "
|
|
package.path = '$hypr_dir/?.lua;' .. package.path
|
|
hl = {
|
|
monitor = function() end,
|
|
workspace_rule = function(rule)
|
|
print(tostring(rule.workspace) .. ' ' .. tostring(rule.monitor))
|
|
end,
|
|
}
|
|
dofile('$hypr_dir/monitors.lua')
|
|
" 2>/dev/null
|
|
}
|
|
|
|
PRIMARY='{"displays":{"DP-2":{"mode":"4500x3000@60","scale":1.5,"transform":0,"x":0,"y":0,"primary":true}}'
|
|
|
|
# ── Off: Hyprland's own behaviour, which needs no rules at all ────────────────
|
|
|
|
off="$(emit "$PRIMARY,\"workspacesOnPrimaryOnly\":false}")"
|
|
[[ -z "$off" ]] || note "with the setting off, $(wc -l <<<"$off") workspace rules are still emitted"
|
|
|
|
absent="$(emit "$PRIMARY}")"
|
|
[[ -z "$absent" ]] || note 'with the setting absent, workspace rules are emitted anyway'
|
|
|
|
# ── On: every workspace the keybinds reach, and no more ──────────────────────
|
|
|
|
on="$(emit "$PRIMARY,\"workspacesOnPrimaryOnly\":true}")"
|
|
emitted="$(grep -c . <<<"$on" || true)"
|
|
|
|
# The count is not a magic number: it is how many workspaces ALT+1..ALT+0 reach.
|
|
# If keybinds.lua ever binds a different number, pinning the old count leaves
|
|
# some workspaces pinned and others not, which is worse than either.
|
|
bound="$(sed -n 's/.*for i = 1, \([0-9]*\) do.*/\1/p' "$keybinds" | head -1)"
|
|
[[ -n "$bound" ]] || bound=10
|
|
|
|
(( emitted == bound )) \
|
|
|| note "the setting pins $emitted workspaces but the keybinds reach $bound"
|
|
|
|
while read -r workspace monitor; do
|
|
[[ -n "$workspace" ]] || continue
|
|
[[ "$monitor" == "DP-2" ]] \
|
|
|| note "workspace $workspace is pinned to '$monitor' rather than the primary display"
|
|
done <<<"$on"
|
|
|
|
# ── On, with nothing to pin to ───────────────────────────────────────────────
|
|
#
|
|
# A machine can have the preference set and no primary recorded -- it is the
|
|
# state this one is in. Guessing a primary would move every workspace onto
|
|
# whichever output happened to sort first.
|
|
|
|
no_primary="$(emit '{"workspacesOnPrimaryOnly":true}')"
|
|
[[ -z "$no_primary" ]] \
|
|
|| note 'with no primary display recorded, workspaces are pinned to a guess'
|
|
|
|
# ── The preference cannot pretend to be an option ────────────────────────────
|
|
|
|
if grep -q 'key: "workspacesOnPrimaryOnly"' "$schema"; then
|
|
block="$(sed -n '/key: "workspacesOnPrimaryOnly"/,/^ },/p' "$schema")"
|
|
grep -q 'hypr:' <<<"$block" \
|
|
&& note 'workspacesOnPrimaryOnly declares a hypr option, but workspace rules are not settable options'
|
|
else
|
|
note 'workspacesOnPrimaryOnly is not in the schema'
|
|
fi
|
|
|
|
# ── The page tells the truth ─────────────────────────────────────────────────
|
|
|
|
grep -q 'Displays.monitors.length >= 2' "$page" \
|
|
|| note 'the Workspaces card is not hidden on a single-display machine'
|
|
|
|
# Applied has to be read back from the compositor. Inferring it from the
|
|
# preference having been written is how a page comes to claim a setting is in
|
|
# effect when it is waiting on a reload.
|
|
grep -q 'hyprctl", "-j", "workspacerules' "$service" \
|
|
|| note 'the service never reads the compositor, so it cannot know whether the setting took effect'
|
|
grep -q '"hyprctl", "reload"' "$service" \
|
|
|| note 'the service has no way to apply the setting'
|
|
|
|
# ── Report ───────────────────────────────────────────────────────────────────
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
|
|
printf 'workspace rules contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'workspace rules contract: PASS (%d workspaces pinned when enabled)\n' "$emitted"
|