Files
Panama/tests/hypr/workspace-rules-contract
T
Gabriel Brown 4c77bc2f61 Decide whether the other screens join in on workspaces
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
2026-08-21 02:16:29 -04:00

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"