#!/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"