A floating window on the console is not laid out by the gaps, so it no longer stretches the panel to full width. Moving an app onto or off the scratchpad and toggling floating now refit too, via window.move_to_workspace and window.update_rules; both were measured on Hyprland 0.56.2 to carry the settled count. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012UyVoFTM98Tduoxg7qZax7
280 lines
14 KiB
Bash
Executable File
280 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
|
|
|
require_command lua
|
|
|
|
# The console is sized by the gaps around it, recomputed from the monitor,
|
|
# because a window rule's size would freeze at whatever the screen measured when
|
|
# the console first opened. The arithmetic is what keeps it a half-height panel
|
|
# on a scaled display, so it is worth pinning down.
|
|
# base-test.sh does not set -e, so the assertions have to fail the file
|
|
# themselves rather than leaving the pass below to run regardless.
|
|
OMARCHY_PATH="$ROOT" lua - <<'LUA' || fail "the console is a centered panel until a second app joins it"
|
|
local rules, handlers = {}, {}
|
|
local monitor = nil
|
|
local workspace = nil
|
|
|
|
hl = {
|
|
config = function() end,
|
|
animation = function() end,
|
|
workspace_rule = function(rule) table.insert(rules, rule) end,
|
|
on = function(event, callback) handlers[event] = callback end,
|
|
get_active_monitor = function() return monitor end,
|
|
get_workspace = function() return workspace end,
|
|
-- workspace.windows is the tiled count here and workspace.floats the floating
|
|
-- one, so the fixtures can say which kind of window is on the console.
|
|
get_workspace_windows = function()
|
|
local list = {}
|
|
for _ = 1, workspace and workspace.windows or 0 do table.insert(list, { floating = false }) end
|
|
for _ = 1, workspace and workspace.floats or 0 do table.insert(list, { floating = true }) end
|
|
return list
|
|
end,
|
|
exec_scheduled_prop_refresh_immediately = function() end,
|
|
}
|
|
|
|
dofile(os.getenv("OMARCHY_PATH") .. "/default/hypr/bootstrap.lua")
|
|
require("default.hypr.qconsole")
|
|
|
|
local function current()
|
|
return rules[#rules]
|
|
end
|
|
|
|
local function gaps()
|
|
local g = current().gaps_out
|
|
return g.top, g.right, g.bottom, g.left
|
|
end
|
|
|
|
-- Config loads before the outputs are up, so the first pass has no monitor to
|
|
-- read. It still has to leave a rule behind, or the console would open unseeded.
|
|
assert(#rules > 0, "console is ruled even before a monitor can be read")
|
|
assert(current().on_created_empty:find("omarchy%-agent"), "console is seeded with the default agent")
|
|
assert(current().on_created_empty:find("^%[workspace special:scratchpad silent%]"),
|
|
"the seed is pinned to the console rather than trusting the spawn to inherit it")
|
|
assert(current().workspace == "special:scratchpad")
|
|
|
|
local function rescale(height, scale, bar)
|
|
monitor = { width = 1920, height = height, scale = scale, transform = 0, reserved = { top = bar, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
return current().gaps_out.bottom
|
|
end
|
|
|
|
-- Same panel, same logical size, different scale: the console must not care.
|
|
assert(rescale(1080, 1, 40) == 520, "half of a 1080p work area, unscaled")
|
|
assert(rescale(2160, 2, 40) == 520, "the same half once the monitor is scaled 2x")
|
|
assert(rescale(2160, 1.5, 40) == 700, "and at a fractional scale")
|
|
|
|
-- The bar is already out of the work area; counting it twice would push the
|
|
-- console short.
|
|
assert(rescale(1440, 1, 0) == 720, "a monitor with nothing reserved")
|
|
|
|
local final = current()
|
|
assert(final.gaps_out.top == 0, "the console stays flush with the top, the way a drop-down arrives")
|
|
assert(final.gaps_out.left == final.gaps_out.right, "the panel is centered")
|
|
assert(final.on_created_empty:find("omarchy%-agent"), "refitting keeps the console seeded")
|
|
assert(final.no_border == true, "the console drops the active window border")
|
|
|
|
-- A monitor that cannot be read must not wipe the last good rule.
|
|
local before = current().gaps_out.bottom
|
|
monitor = nil
|
|
handlers["monitor.layout_changed"]()
|
|
assert(current().gaps_out.bottom == before, "an absent monitor leaves the console as it was")
|
|
|
|
-- A monitor handle outliving its output answers nil to everything, which is
|
|
-- what a layout change looks like mid-flight. Reading height or reserved off
|
|
-- that would throw, so the scale guard has to catch it first.
|
|
local expired = setmetatable({}, { __index = function() return nil end })
|
|
monitor = expired
|
|
handlers["monitor.layout_changed"]()
|
|
assert(current().gaps_out.bottom == before, "an expired monitor handle is not read to pieces")
|
|
|
|
-- Refitting to the size it already is would still cost a state refresh, and
|
|
-- monitor.focused fires on every hop between screens.
|
|
monitor = { width = 2560, height = 1440, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
local written = #rules
|
|
handlers["monitor.focused"]()
|
|
handlers["monitor.layout_changed"]()
|
|
assert(#rules == written, "refitting to the same size does not rewrite the rule")
|
|
|
|
-- A centered 2:1 panel, not a full-width drop-down.
|
|
monitor = { width = 1920, height = 1080, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
local top, right, bottom, left = gaps()
|
|
assert(top == 0 and left == 420 and right == 420 and bottom == 540, "16:9 leaves a 1080x540 panel")
|
|
|
|
local dell = { name = "DP-1", width = 6144, height = 2560, scale = 1, transform = 0, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
|
monitor = dell
|
|
handlers["monitor.layout_changed"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 1807 and right == 1807 and bottom == 1265, "the same 2:1 panel on 6K")
|
|
|
|
-- Same logical box at scale 2x (physical 12288x5120): scale does not change the
|
|
-- panel's logical size.
|
|
monitor = { name = "DP-1", width = 12288, height = 5120, scale = 2, transform = 0, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 1807 and right == 1807 and bottom == 1265, "the 6K box is in logical pixels")
|
|
|
|
-- Same height, different width: the sides have to move even though the bottom
|
|
-- gap is identical, so the cache cannot key on height alone.
|
|
monitor = { width = 2560, height = 1440, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
written = #rules
|
|
monitor = { width = 3440, height = 1440, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
assert(#rules == written + 1, "a same-height ultrawide hop still rewrites the sides")
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 1000 and right == 1000 and bottom == 720, "3440x1440 leaves a 1440x720 box")
|
|
|
|
-- A panel wider than the screen is just the screen: a portrait monitor has no
|
|
-- room for a 2:1 box and falls back to the full width rather than a negative gap.
|
|
monitor = { width = 1080, height = 1920, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 0 and right == 0 and bottom == 960, "a portrait monitor keeps the full width")
|
|
assert(left >= 0 and right >= 0 and bottom >= 0, "gaps are never negative")
|
|
|
|
-- A monitor turned on its side still reports the panel's own pixels, so the
|
|
-- work area has to be turned with it. Measured against Hyprland 0.56.2: a
|
|
-- rotated 1920x1080 lays its windows out in 1080x1920 while width and height
|
|
-- still read 1920 and 1080. Quarter turns are the odd transforms; a half turn
|
|
-- leaves the shape alone.
|
|
monitor = { width = 1920, height = 1080, scale = 1, transform = 1, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 0 and right == 0 and bottom == 945, "a quarter-turned monitor is sized portrait")
|
|
|
|
monitor = { width = 1920, height = 1080, scale = 1, transform = 3, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 0 and right == 0 and bottom == 945, "and so is the other quarter turn")
|
|
|
|
monitor = { width = 1920, height = 1080, scale = 1, transform = 2, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 435 and right == 435 and bottom == 525, "a half turn is still landscape")
|
|
|
|
-- Special workspaces open on the monitor they are toggled on, not on whichever
|
|
-- output was focused when the rule was last written. Opening on 1080p after a
|
|
-- 6K fit has to resize the box.
|
|
local acer = { name = "HDMI-A-1", width = 1920, height = 1080, scale = 1, transform = 0, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
|
|
monitor = dell
|
|
handlers["monitor.layout_changed"]()
|
|
handlers["workspace.special_active"]({ name = "special:scratchpad" }, acer)
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 435 and right == 435 and bottom == 525, "opening on 1080p after a 6K fit resizes the box")
|
|
assert(1920 - left - right > 0 and 1080 - 30 - bottom > 0, "1080p leftover is never negative")
|
|
|
|
-- follow_mouse onto the 6K while the console is already showing on 1080p must
|
|
-- not steal the rule; that is what oversized the Dell after a hop.
|
|
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 1 }
|
|
monitor = dell
|
|
written = #rules
|
|
handlers["monitor.focused"](dell)
|
|
assert(#rules == written, "focus on another output does not rewrite an open console")
|
|
|
|
-- The output the console is showing on can go away mid-layout-change. Its
|
|
-- handle then answers nil to everything, and preferring it blindly would leave
|
|
-- the console stranded at the gaps of the monitor that is gone. The rule is
|
|
-- still the 1080p one here, so only refitting on the Dell can satisfy this.
|
|
workspace = { name = "special:scratchpad", visible = true, monitor = expired, windows = 1 }
|
|
monitor = dell
|
|
handlers["monitor.layout_changed"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 1807 and right == 1807 and bottom == 1265,
|
|
"a console whose output vanished refits on the monitor that is still there")
|
|
|
|
-- Back onto the 1080p panel for the window-count checks below.
|
|
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 1 }
|
|
monitor = acer
|
|
handlers["monitor.layout_changed"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 435 and right == 435 and bottom == 525, "and refits again once it is back on a live output")
|
|
|
|
-- One window reads as a console and keeps the panel. A second app has turned
|
|
-- the scratchpad into a workspace, and a workspace wants the whole width.
|
|
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 2 }
|
|
handlers["window.open"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 0 and right == 0, "a second app on the scratchpad restores the full width")
|
|
assert(bottom == 525, "and the console keeps its half-height drop")
|
|
|
|
workspace.windows = 3
|
|
written = #rules
|
|
handlers["window.open"]()
|
|
assert(#rules == written, "a third app changes nothing that is already full width")
|
|
|
|
workspace.windows = 1
|
|
handlers["window.destroy"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 435 and right == 435, "closing back down to one window recenters the panel")
|
|
|
|
-- An empty scratchpad is about to be seeded with a single agent, so it is sized
|
|
-- as a console rather than as a workspace.
|
|
workspace.windows = 0
|
|
handlers["window.destroy"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 435 and right == 435, "an empty console is still a console")
|
|
|
|
-- A hidden console is refitted on its way back in, so the count does not have to
|
|
-- be chased while it is off screen; every window on the desktop would otherwise
|
|
-- rewrite the rule.
|
|
workspace = { name = "special:scratchpad", visible = false, monitor = acer, windows = 4 }
|
|
monitor = dell
|
|
written = #rules
|
|
handlers["window.open"]()
|
|
assert(#rules == written, "a window opening elsewhere does not rewrite a hidden console")
|
|
|
|
-- A scratchpad nothing has opened yet has no workspace to read at all, and is
|
|
-- sized as the console the seed is about to put a single agent into.
|
|
workspace = nil
|
|
monitor = { width = 1920, height = 1080, scale = 1, transform = 0, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
|
|
handlers["monitor.layout_changed"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 420 and right == 420 and bottom == 540, "a scratchpad that does not exist yet is sized as a console")
|
|
|
|
-- Back to a console on screen for the move and float checks.
|
|
workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 1 }
|
|
monitor = acer
|
|
handlers["monitor.layout_changed"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 435 and right == 435, "a single tiled window is a console")
|
|
|
|
-- A floating window on top of the console is not laid out by the gaps, so it
|
|
-- must not stretch the panel out from under the agent.
|
|
workspace.floats = 1
|
|
handlers["window.open"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 435 and right == 435, "a floating window on the console keeps the panel")
|
|
|
|
-- Tiling that float (Super+T) makes it a second app, and floating it again
|
|
-- gives the panel back. Both arrive as window.update_rules.
|
|
workspace.floats, workspace.windows = 0, 2
|
|
handlers["window.update_rules"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 0 and right == 0, "tiling a float on the console restores the full width")
|
|
|
|
workspace.floats, workspace.windows = 1, 1
|
|
handlers["window.update_rules"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 435 and right == 435, "floating it again recenters the panel")
|
|
|
|
-- Sending an app onto the scratchpad (Super+Alt+S) or off it (Super+Shift+1)
|
|
-- does not open or destroy anything, so the move itself has to refit.
|
|
workspace.floats, workspace.windows = 0, 2
|
|
handlers["window.move_to_workspace"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 0 and right == 0, "moving a second app onto the console restores the full width")
|
|
|
|
workspace.windows = 1
|
|
handlers["window.move_to_workspace"]()
|
|
top, right, bottom, left = gaps()
|
|
assert(left == 435 and right == 435, "moving it back off recenters the panel")
|
|
|
|
-- window.close still counts the window on its way out, and window.destroy
|
|
-- follows it with the settled count, so only destroy is hooked.
|
|
assert(handlers["window.close"] == nil, "window.close counts the window on its way out")
|
|
LUA
|
|
pass "the console is a centered panel until a second app joins it"
|