Files
omarchy/test/shell.d/hyprland-floating-workspaces-test.sh
T
David Heinemeier HanssonandClaude Opus 5.5 e71a816e90 Cache workspace layouts instead of running a script on every switch (#15028)
* Cache workspace layouts instead of running a script on every switch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test the workspace layout cache by running its logic, not matching its source

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the fake layout read running while its exit handler runs

Addresses Codex review: clearing running first let an undeferred, broken retry pass the cache tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:03:30 +09:00

527 lines
22 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
require_command lua
require_command jq
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
stub_dir="$tmpdir/bin"
fake="$tmpdir/hyprland"
home_dir="$tmpdir/home"
state_home="$home_dir/.local/state"
runtime_dir="$tmpdir/runtime"
modes_dir="$state_home/omarchy/workspace-layouts"
mkdir -p "$stub_dir" "$fake" "$home_dir" "$runtime_dir"
# Answers come from files the cases write, and everything else is logged.
cat >"$stub_dir/hyprctl" <<'EOF'
#!/bin/bash
case "$1" in
activeworkspace) cat "$FAKE/activeworkspace.json" ;;
activewindow) cat "$FAKE/activewindow.json" ;;
clients) cat "$FAKE/clients.json" ;;
monitors) cat "$FAKE/monitors.json" ;;
workspaces) cat "$FAKE/workspaces.json" ;;
plugin) printf '[]\n' ;;
getoption) printf '{"option": "%s", "str": "%s"}\n' "$2" "${HYPRCTL_LAYOUT:-dwindle}" ;;
*) printf '%s\n' "$*" >>"$FAKE/hyprctl.log" ;;
esac
EOF
for command in omarchy-notification-send omarchy-bar omarchy-shell; do
printf '#!/bin/bash\nprintf "%%s\\n" "%s $*" >>"$FAKE/commands.log"\n' "$command" >"$stub_dir/$command"
done
chmod +x "$stub_dir"/*
run() {
env HOME="$home_dir" XDG_STATE_HOME="$state_home" XDG_RUNTIME_DIR="$runtime_dir" FAKE="$fake" \
PATH="$stub_dir:$ROOT/bin:$PATH" "$@"
}
dispatched() {
grep -F "dispatch hl.dsp.$1" "$fake/hyprctl.log" >/dev/null
}
reset_logs() {
: >"$fake/hyprctl.log"
: >"$fake/commands.log"
}
cat >"$fake/monitors.json" <<'EOF'
[{ "id": 0, "name": "DP-1", "x": 0, "y": 0, "width": 2560, "height": 1440, "scale": 1, "transform": 0, "reserved": [0, 30, 0, 0] }]
EOF
printf '{ "id": 2, "name": "2", "tiledLayout": "dwindle" }\n' >"$fake/activeworkspace.json"
printf '[{ "id": 2, "name": "2", "tiledLayout": "dwindle" }, { "id": 4, "name": "4", "tiledLayout": "scrolling" }]\n' >"$fake/workspaces.json"
reset_logs
# --- Which layout a workspace is in -------------------------------------------
[[ $(run omarchy-hyprland-workspace-layout-current) == "dwindle" ]] ||
fail "a workspace with nothing saved reports Hyprland's tiled layout"
[[ $(run omarchy-hyprland-workspace-layout-current 4) == "scrolling" ]] ||
fail "another workspace reports its own tiled layout"
mkdir -p "$modes_dir"
printf 'o.workspace_mode({ workspace = "4", mode = "floating" })\n' >"$modes_dir/4.lua"
[[ $(run omarchy-hyprland-workspace-layout-current 4) == "floating" ]] ||
fail "a saved floating mode is reported, though Hyprland cannot report it"
pass "the layout of a workspace comes from its saved mode before Hyprland's"
# --- The default layout -------------------------------------------------------
[[ $(run omarchy-hyprland-workspace-layout-default) == "dwindle" ]] || fail "the default layout starts as tiling"
[[ $(HYPRCTL_LAYOUT=scrolling run omarchy-hyprland-workspace-layout-default) == "scrolling" ]] ||
fail "without a saved default, Hyprland's own layout is the default"
run omarchy-hyprland-workspace-layout-default scrolling
grep -qxF 'o.workspace_mode({ default = "scrolling" })' "$modes_dir/default.lua" ||
fail "the default layout is saved" "$(cat "$modes_dir/default.lua" 2>&1)"
[[ $(run omarchy-hyprland-workspace-layout-default) == "scrolling" ]] || fail "the saved default is reported"
grep -qx 'reload' "$fake/hyprctl.log" || fail "a new default reloads Hyprland"
grep -qF 'omarchy-shell -q omarchy.workspaces refresh' "$fake/commands.log" || fail "a new default refreshes the bar"
pass "the default layout is saved, applied, and shown"
run omarchy-hyprland-workspace-layout-default floating
[[ $(run omarchy-hyprland-workspace-layout-current 2) == "floating" ]] ||
fail "a workspace without a layout of its own follows the default"
[[ $(run omarchy-hyprland-workspace-layout-current 4) == "floating" ]] ||
fail "a saved floating workspace still reports floating"
printf 'o.workspace_mode({ workspace = "4", mode = "dwindle" })\n' >"$modes_dir/4.lua"
[[ $(run omarchy-hyprland-workspace-layout-current 4) == "dwindle" ]] ||
fail "a workspace given its own layout keeps it under a floating default"
pass "workspaces without a layout of their own follow the default, others keep theirs"
if run omarchy-hyprland-workspace-layout-default sideways 2>/dev/null; then
fail "an unknown default layout is refused"
fi
[[ $(run omarchy-hyprland-workspace-layout-default) == "floating" ]] || fail "a refused default leaves the saved one alone"
pass "an unknown default layout is refused"
rm -f "$modes_dir"/*.lua
# --- The workspace mode in Hyprland --------------------------------------------
mkdir -p "$state_home/omarchy/current/theme"
cat >"$state_home/omarchy/current/theme/colors.toml" <<'EOF'
background = "#112233"
lighter_background = "#223344"
selection = "#334455"
foreground = "#ddeeff"
accent = "#ff8800"
EOF
lua_harness="$tmpdir/harness.lua"
cat >"$lua_harness" <<'LUA'
local all = {}
local subscriptions = {}
local plugin_loads = 0
local titlebar_config = nil
local buttons = {}
local config = { ["input.follow_mouse"] = 1, ["decoration.shadow.enabled"] = false }
local rules = {}
local placed = {}
local active = { id = 1 }
local titlebars_installed = os.getenv("TITLEBARS_INSTALLED") == "1"
local monitor = { width = 2560, height = 1440, scale = 1, transform = 0, reserved = { left = 0, right = 0, top = 30, bottom = 0 } }
function window(address, workspace, options)
options = options or {}
local created = {
address = address,
floating = options.floating or false,
fullscreen = options.fullscreen or 0,
pinned = false,
tags = options.tags or {},
workspace = { id = workspace },
monitor = monitor,
}
table.insert(all, created)
return created
end
function has_tag(target, tag)
for _, held in ipairs(target.tags) do
if held == tag then
return true
end
end
return false
end
local real_open = io.open
io.open = function(path, mode)
if path == "/usr/lib/omarchy-hyprland-titlebars/titlebars.so" then
return titlebars_installed and { close = function() end } or nil
end
return real_open(path, mode)
end
hl = {
workspace_rule = function() end,
window_rule = function(rule) table.insert(rules, rule) end,
get_workspace_windows = function(selector)
local found = {}
for _, candidate in ipairs(all) do
if tostring(candidate.workspace.id) == tostring(selector) then
table.insert(found, candidate)
end
end
return found
end,
get_windows = function(filter)
local found = {}
for _, candidate in ipairs(all) do
if not filter or (filter.tag and has_tag(candidate, filter.tag)) then
table.insert(found, candidate)
end
end
return found
end,
dispatch = function(action)
local target = action.window
if action.kind == "place" then
placed[target.address] = true
elseif action.tag then
local name = action.tag:sub(2)
if action.tag:sub(1, 1) == "+" then
if not has_tag(target, name) then
table.insert(target.tags, name)
end
else
for index, held in ipairs(target.tags) do
if held == name then
table.remove(target.tags, index)
break
end
end
end
else
target.floating = action.action == "on"
end
end,
dsp = {
window = {
float = function(spec) return spec end,
tag = function(spec) return spec end,
resize = function(spec) spec.kind = "place" return spec end,
center = function(spec) spec.kind = "place" return spec end,
move = function(spec) spec.kind = "place" return spec end,
},
},
on = function(event, callback)
subscriptions[event] = subscriptions[event] or {}
table.insert(subscriptions[event], callback)
end,
timer = function(callback) callback() end,
get_active_workspace = function() return active end,
get_config = function(key) return config[key] end,
config = function(values)
if values.plugin then
titlebar_config = values.plugin.hyprbars
return
end
local function flatten(prefix, options)
for key, value in pairs(options) do
if type(value) == "table" then
flatten(prefix .. key .. ".", value)
else
config[prefix .. key] = value
end
end
end
flatten("", values)
end,
plugin = {
load = function(path)
plugin_loads = plugin_loads + 1
assert(path == "/usr/lib/omarchy-hyprland-titlebars/titlebars.so", "the titlebars were loaded from the wrong place")
if os.getenv("TITLEBARS_LOADED") == "1" then
hl.plugin.hyprbars = { add_button = function(button) table.insert(buttons, button) end }
end
end,
},
}
function state()
return {
plugin_loads = plugin_loads,
titlebar_config = titlebar_config,
buttons = buttons,
config = config,
rules = rules,
placed = placed,
subscriptions = subscriptions,
active = active,
}
end
dofile(os.getenv("OMARCHY_PATH") .. "/default/hypr/bootstrap.lua")
require("default.hypr.helpers")
LUA
run_lua() {
local script=$1
shift
printf 'dofile("%s")\n%s\n' "$lua_harness" "$script" >"$tmpdir/case.lua"
env HOME="$home_dir" XDG_STATE_HOME="$state_home" OMARCHY_PATH="$ROOT" "$@" lua "$tmpdir/case.lua"
}
rm -rf "$modes_dir"
# Nothing floats: the titlebar plugin stays out and nothing changes.
if ! run_lua '
local tiled = window("0x1", 1)
require("default.hypr.workspace-layouts")
local s = state()
assert(s.plugin_loads == 0, "the titlebar plugin was loaded with nothing floating")
assert(not tiled.floating, "a window floated with nothing floating")
assert(#s.rules == 0, "window rules were added with nothing floating")
' TITLEBARS_INSTALLED=1; then
fail "nothing changes for a session where no workspace floats"
fi
pass "nothing changes for a session where no workspace floats"
# A floating default floats every regular workspace without a mode of its own,
# and leaves special workspaces and those with their own layout alone.
mkdir -p "$modes_dir"
printf 'o.workspace_mode({ default = "floating" })\n' >"$modes_dir/default.lua"
printf 'o.workspace_mode({ workspace = "2", mode = "dwindle" })\n' >"$modes_dir/2.lua"
if ! run_lua '
local first = window("0x1", 1)
local dialog = window("0x2", 1, { floating = true })
local own_layout = window("0x3", 2)
local scratch = window("0x4", -98)
local fullscreen = window("0x5", 3, { fullscreen = 1 })
require("default.hypr.workspace-layouts")
local s = state()
assert(first.floating and has_tag(first, "omarchy-mode-floated"), "a window on an unsaved workspace was not floated")
assert(s.placed["0x1"], "a window the mode floated was not given a place")
assert(has_tag(first, "omarchy-floating-workspace"), "a floated window got no titlebar tag")
assert(has_tag(dialog, "omarchy-floating-workspace"), "a window floating already got no titlebar tag")
assert(not has_tag(dialog, "omarchy-mode-floated"), "a window floating already was claimed")
assert(not s.placed["0x2"], "a window floating already was moved")
assert(not own_layout.floating, "a workspace with its own layout was floated")
assert(not has_tag(own_layout, "omarchy-floating-workspace"), "a tiled window got a titlebar tag")
assert(not scratch.floating and #scratch.tags == 0, "a window on a special workspace was touched")
assert(not fullscreen.floating, "a fullscreen window was floated")
assert(has_tag(fullscreen, "omarchy-floating-workspace"), "a fullscreen window on a floating workspace got no titlebar tag")
-- Pointer focus follows the active workspace.
assert(s.config["input.follow_mouse"] == 0, "a floating workspace focused on hover")
s.active.id = 2
s.subscriptions["workspace.active"][1]()
assert(s.config["input.follow_mouse"] == 1, "a tiled workspace lost the user focus setting")
-- Shadows only for windows on floating workspaces, when the user has none.
assert(s.config["decoration.shadow.enabled"] == true, "floating windows got no shadows to tell them apart")
assert(#s.rules >= 3, "the floating window rules were not added")
' TITLEBARS_INSTALLED=1; then
fail "a floating default floats every regular workspace without a layout of its own"
fi
pass "a floating default floats every regular workspace without a layout of its own"
# A tiled default is Hyprland's own layout for workspaces without a rule.
rm -f "$modes_dir"/*.lua
printf 'o.workspace_mode({ default = "scrolling" })\n' >"$modes_dir/default.lua"
if ! run_lua '
require("default.hypr.workspace-layouts")
assert(state().config["general.layout"] == "scrolling", "a scrolling default did not set the layout: " .. tostring(state().config["general.layout"]))
'; then
fail "a scrolling default is Hyprland's default layout"
fi
pass "a scrolling default is Hyprland's default layout"
# Moving the default away from floating is a fresh start, and the windows it floated
# have to be given back even though nothing names their workspace any more. With
# no workspace given a layout of its own, no saved mode is left to start the mode.
rm -f "$modes_dir"/*.lua
if ! run_lua '
local floated = window("0x1", 1, { floating = true, tags = { "omarchy-mode-floated", "omarchy-floating-workspace" } })
local dialog = window("0x2", 1, { floating = true, tags = { "omarchy-floating-workspace" } })
local scratch = window("0x3", -98, { floating = true, tags = { "omarchy-mode-floated" } })
require("default.hypr.workspace-layouts")
assert(not floated.floating, "a window the mode floated stayed floating once floating was off")
assert(not has_tag(floated, "omarchy-mode-floated"), "a window given back is still claimed")
assert(not has_tag(floated, "omarchy-floating-workspace"), "a tiled window kept its titlebar tag")
assert(dialog.floating, "a dialog was tiled when floating was turned off")
assert(not has_tag(dialog, "omarchy-floating-workspace"), "a dialog on a tiled workspace kept its titlebar tag")
assert(scratch.floating and has_tag(scratch, "omarchy-mode-floated"), "a window on the scratchpad was tiled")
'; then
fail "turning floating off gives back what it floated, but not what is on the scratchpad"
fi
pass "turning floating off gives back what it floated, but not what is on the scratchpad"
# A tiled window that was fullscreen when its workspace started floating is
# left as it is, and floats once it comes out of fullscreen.
printf 'o.workspace_mode({ workspace = "1", mode = "floating" })\n' >"$modes_dir/1.lua"
if ! run_lua '
local video = window("0x1", 1, { fullscreen = 2 })
require("default.hypr.workspace-layouts")
assert(not video.floating, "a fullscreen window was floated")
video.fullscreen = 0
state().subscriptions["window.fullscreen"][1](video)
assert(video.floating and has_tag(video, "omarchy-mode-floated"), "a window leaving fullscreen on a floating workspace stayed tiled")
'; then
fail "a window leaving fullscreen on a floating workspace floats"
fi
pass "a window leaving fullscreen on a floating workspace floats"
# A window moved to a special workspace, like the scratchpad, is left alone.
printf 'o.workspace_mode({ workspace = "1", mode = "floating" })\n' >"$modes_dir/1.lua"
if ! run_lua '
local moving = window("0x1", 1)
require("default.hypr.workspace-layouts")
local moved = state().subscriptions["window.move_to_workspace"][1]
assert(moving.floating, "the window was not floated to begin with")
moved(moving, { id = -98 })
assert(moving.floating and has_tag(moving, "omarchy-mode-floated"), "a window was tiled on its way to the scratchpad")
moved(moving, { id = 2 })
assert(not moving.floating, "a window carried to a tiled workspace kept floating")
'; then
fail "the mode leaves windows on their way to a special workspace alone"
fi
pass "the mode leaves windows on their way to a special workspace alone"
# Titlebars: asked for only when installed, and themed once loaded.
if ! run_lua '
window("0x1", 1)
require("default.hypr.workspace-layouts")
assert(state().plugin_loads == 0, "titlebars were asked for without being installed")
'; then
fail "a missing titlebar package leaves floating without titlebars"
fi
if ! run_lua '
window("0x1", 1)
require("default.hypr.workspace-layouts")
local s = state()
assert(s.plugin_loads == 1, "the titlebar plugin was not loaded with a workspace floating")
local bar = s.titlebar_config
assert(bar, "the titlebars were not configured once loaded")
assert(bar.workspace_tag == "omarchy-floating-workspace", "titlebars are not limited to floating workspaces")
assert(bar.bar_color == "rgb(223344)", "titlebars do not stand out from the window background: " .. tostring(bar.bar_color))
assert(bar.inactive_button_color == "rgb(223344)", "titlebar buttons do not match the titlebar")
for _, button in ipairs(s.buttons) do
assert(button.bg_color == "rgb(223344)", "a titlebar button does not match the titlebar")
end
assert(bar.col.text == "rgb(ddeeff)", "titlebar text does not follow the theme foreground")
assert(not bar.edge_snap, "dragged windows snap to screen edges")
assert(#s.buttons == 2, "the titlebars do not have exactly close and maximize")
local actions = s.buttons[1].action .. s.buttons[2].action
assert(actions:find("window.close", 1, true), "no button closes the window")
assert(actions:find("maximized", 1, true), "no button maximizes the window")
assert(bar.on_double_click:find("maximized", 1, true), "double-clicking a titlebar does not maximize")
' TITLEBARS_INSTALLED=1 TITLEBARS_LOADED=1; then
fail "titlebars load when a workspace floats and follow the theme"
fi
pass "titlebars load when a workspace floats and follow the theme"
# A theme whose raised surface is its background would hide the titlebar again,
# so it takes the selection color instead.
theme="$state_home/omarchy/current/theme/colors.toml"
cp "$theme" "$theme.saved"
sed -i 's/^lighter_background = .*/lighter_background = "#112233"/' "$theme"
if ! run_lua '
window("0x1", 1)
require("default.hypr.workspace-layouts")
local bar = state().titlebar_config
assert(bar and bar.bar_color == "rgb(334455)", "a flat theme does not fall back to its selection color: " .. tostring(bar and bar.bar_color))
' TITLEBARS_INSTALLED=1 TITLEBARS_LOADED=1; then
fail "titlebars stand out on a theme whose raised surface is its background"
fi
mv "$theme.saved" "$theme"
pass "titlebars stand out on a theme whose raised surface is its background"
# omarchy-theme-set writes the current theme under ~/.local/state even when
# XDG_STATE_HOME points elsewhere, so that is where the titlebars find it.
other_state="$tmpdir/other-state"
mkdir -p "$other_state/omarchy/workspace-layouts"
cp "$modes_dir/1.lua" "$other_state/omarchy/workspace-layouts/"
if ! run_lua '
window("0x1", 1)
require("default.hypr.workspace-layouts")
local bar = state().titlebar_config
assert(bar and bar.bar_color == "rgb(223344)", "titlebars missed the theme with XDG_STATE_HOME elsewhere: " .. tostring(bar and bar.bar_color))
' TITLEBARS_INSTALLED=1 TITLEBARS_LOADED=1 XDG_STATE_HOME="$other_state"; then
fail "titlebars follow the theme when XDG_STATE_HOME points elsewhere"
fi
pass "titlebars follow the theme when XDG_STATE_HOME points elsewhere"
run_node_test <<'JS'
const source = require('fs').readFileSync(path.join(root, 'shell/plugins/bar/widgets/Workspaces.qml'), 'utf8')
assert(source.includes('function onFocusedWorkspaceChanged() { root.showFocusedMode() }'), 'a workspace switch shows the cached layout instead of running the script')
assert(source.includes('String(event.name) === "configreloaded") root.refresh()'), 'a config reload drops every cached layout')
// Run the widget's own cache logic against a fake Hyprland and process.
function body(pattern) {
const match = source.match(pattern)
if (!match) fail('Workspaces.qml cache logic can be extracted', String(pattern))
return match[1]
}
// QML resolves bare calls like readMode() on the widget, so it is passed in too.
const args = ['root', 'Hyprland', 'modeProc', 'Qt', 'readMode']
const fn = name => new Function(...args, body(new RegExp(`\\n function ${name}\\(\\) \\{([\\s\\S]*?)\\n \\}`)))
const streamFinished = new Function(...args, 'text', body(/onStreamFinished: \{([\s\S]*?)\n \}/))
const exited = new Function(...args, body(/onExited: \{([\s\S]*?)\n \}/))
const hyprland = { focusedWorkspace: { id: 1 } }
const proc = { running: false, command: null, runs: [] }
const deferred = []
const qt = { callLater: callback => deferred.push(callback) }
const widget = { focusedMode: 'dwindle', modeStale: false, modeCache: {}, modeGeneration: 0, modeProcWorkspace: -1, modeProcGeneration: 0 }
for (const name of ['focusedWorkspaceId', 'showFocusedMode', 'readMode', 'refresh']) {
const call = fn(name)
widget[name] = () => call(widget, hyprland, proc, qt, widget.readMode)
}
const start = () => { if (proc.running) proc.runs.push(proc.command[1]) }
// Quickshell still reports the process running while it signals exit, so a
// retry has to be deferred until after the handler returns.
const finish = output => {
streamFinished(widget, hyprland, proc, qt, widget.readMode, output)
exited(widget, hyprland, proc, qt, widget.readMode)
proc.running = false
while (deferred.length) deferred.shift()()
}
const focus = id => { hyprland.focusedWorkspace = { id }; widget.showFocusedMode(); start() }
widget.refresh(); start()
finish('scrolling\n')
assertEqual(widget.focusedMode, 'scrolling', 'the first read shows the focused layout')
focus(2); finish('floating\n')
focus(1)
assertEqual(widget.focusedMode, 'scrolling', 'returning to a read workspace shows its cached layout')
assertDeepEqual(proc.runs, ['1', '2'], 'a cached workspace switch runs no script')
widget.refresh(); start()
assert(proc.running && proc.runs.length === 3, 'a refresh reads the focused layout again')
assertDeepEqual(widget.modeCache, {}, 'a refresh drops every cached layout')
widget.refresh()
assert(widget.modeStale, 'a refresh during a read marks it stale')
finish('dwindle\n')
assert(widget.modeCache[1] === undefined, 'a read that predates a refresh is not cached')
start()
assertDeepEqual(proc.runs, ['1', '2', '1', '1'], 'a stale read is retried once it exits')
finish('floating\n')
assertEqual(widget.focusedMode, 'floating', 'the retried read shows the current layout')
widget.refresh(); start()
focus(2)
assert(widget.modeStale, 'switching to an uncached workspace during a read marks it pending')
finish('scrolling\n')
assertEqual(widget.modeCache[1], 'scrolling', 'the in-flight read still caches its own workspace')
assertEqual(widget.focusedMode, 'floating', 'a read for a workspace no longer focused leaves the shown layout alone')
start()
assertEqual(proc.runs[proc.runs.length - 1], '2', 'the pending read follows the newly focused workspace')
finish('dwindle\n')
assertEqual(widget.focusedMode, 'dwindle', 'the newly focused workspace shows its own layout')
JS