Give the width back when a second app joins the console

The scratchpad spanned the whole screen whatever was on it, which reads as a band rather than a console when it holds the single agent it is usually opened for. It now sizes to a centered panel twice as wide as it is tall, and hands the full width back as soon as a second app is on the scratchpad: two windows splitting a half-width column is worse than the band this replaced.

The panel is still drawn with workspace gaps rather than a window rule, because Hyprland resolves a rule's size once as the window maps and the console has to survive a rescale. It is always flush with the top and always centered, so two numbers describe it, and the memo that suppresses no-op rewrites compares those rather than four edges.

The window count is read on window.open and window.destroy, the two events that run after the workspace's count has settled; window.close and window.move_to_workspace both still count the window on its way out, so refitting from either reads one too many and strands the console at full width. The count is only chased while the console is on screen, since a hidden one is refitted on its way in by workspace.special_active, and every window opened anywhere on the desktop would otherwise rewrite the rule.

Monitor width and height are the panel's own pixels, so an output turned on its side still reports them the way the panel is built rather than the way it hangs. The odd transforms are the quarter turns and those swap the work area: without that a rotated 1920x1080 sized its console from 1920x1080 instead of 1080x1920 and left a 214x1367 sliver.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
This commit is contained in:
committed 2026-08-30 14:50:40 +02:00
1 parent 0bec60f6e4
commit a59afb9659
4 files changed
+220 -184

No files matched your search

-5
View File
@@ -10,11 +10,6 @@ dofile((os.getenv("OMARCHY_PATH") or "/usr/share/omarchy") .. "/default/hypr/boo
-- keeping core window-manager bindings:
-- omarchy_preinstalled_bindings = false
-- Center the quake console in a tiling box. The number is how many times
-- wider the box is than its height. 1 is a square. Comment this out for a
-- full-width drop-down.
omarchy_qconsole_ratio = 2
-- Load Omarchy defaults.
require("default.hypr.omarchy")
+91 -120
View File
@@ -4,7 +4,12 @@
-- How much of the usable screen the console covers, measured from the top.
local share = 0.5
local min_size = 64
-- A console holding a single window is boxed into a centered panel this many
-- times wider than it is tall, rather than stretched the width of the screen.
-- A second app on the scratchpad gets the full width back: two windows splitting
-- a half-width column is worse than the band this replaced.
local box = 2
local SCRATCHPAD = "special:scratchpad"
@@ -26,35 +31,25 @@ hl.config({
},
})
-- The panel is always flush with the top and always centered, so two numbers
-- describe it: the gap down each side and the gap underneath.
--
-- Refitting replaces the rule in place rather than stacking a new one, but it
-- still schedules a monitor and window state refresh, and monitor.focused fires
-- on every hop between screens. Most of those hops do not change the gaps, so
-- only write the rule when it actually moves.
local covering = nil
local beside, below = nil, nil
local function same_gaps(a, b)
return a
and b
and a.top == b.top
and a.right == b.right
and a.bottom == b.bottom
and a.left == b.left
end
local function cover(gaps_out)
if type(gaps_out) == "number" then
gaps_out = { top = 0, right = 0, bottom = gaps_out, left = 0 }
end
if same_gaps(covering, gaps_out) then
local function cover(side, bottom)
if beside == side and below == bottom then
return false
end
covering = gaps_out
beside, below = side, bottom
hl.workspace_rule({
workspace = SCRATCHPAD,
gaps_in = 0,
gaps_out = gaps_out,
gaps_out = { top = 0, right = side, bottom = bottom, left = side },
-- Nothing to highlight in a console that is only ever focused when it is
-- open, and the active border reads as a stray frame around a panel that
@@ -67,145 +62,121 @@ local function cover(gaps_out)
return true
end
local function reserved_edges(monitor)
local reserved = monitor.reserved
if type(reserved) ~= "table" then
return { top = 0, right = 0, bottom = 0, left = 0 }
end
return {
top = reserved.top or 0,
right = reserved.right or 0,
bottom = reserved.bottom or 0,
left = reserved.left or 0,
}
end
-- A positive omarchy_qconsole_ratio (set in hyprland.lua before defaults load)
-- centers the console in a tiling box that many times wider than it is tall.
-- Values below 1 clamp to a square. Unset keeps the full-width drop-down.
local function box_ratio()
local ratio = _G.omarchy_qconsole_ratio
if type(ratio) == "number" and ratio > 0 then
return math.max(1, ratio)
end
return nil
end
local function is_scratchpad(ws)
return ws and (ws.name == SCRATCHPAD or ws.name == "scratchpad")
-- One window reads as a console and gets the panel. A second app has turned the
-- scratchpad into a workspace, and a workspace wants the whole width.
local function alone()
local ws = hl.get_workspace(SCRATCHPAD)
return not ws or ws.windows <= 1
end
-- Sizing the console with a window rule would freeze it at whatever the screen
-- measured when it first opened, because Hyprland resolves those expressions
-- once, as the window maps. Rescaling the monitor afterwards would leave a
-- console that is no longer half of anything. Gaps are re-applied by the layout
-- instead, so the console is sized by the leftover area and that area is
-- recomputed whenever the monitor it is opening on changes.
-- instead, so the console is sized by the area left around it, and that area is
-- recomputed whenever the monitor it opens on changes.
local function fit(monitor)
monitor = monitor or hl.get_active_monitor()
-- A monitor handle whose output has gone away answers nil to every field, and
-- layout changes are exactly when that happens, so this also covers reading
-- height and reserved below.
-- width, height and reserved below.
if not monitor or not monitor.scale or monitor.scale <= 0 then
return false
end
-- Width and height are the panel's own pixels, so a monitor turned on its
-- side still reports them the way the panel is built. The odd transforms are
-- the quarter turns, and those are the ones that swap the work area.
local width, height = monitor.width, monitor.height
if monitor.transform % 2 == 1 then
width, height = height, width
end
-- Monitor dimensions are in physical pixels; gaps are logical, so the scale
-- has to come out before the reserved area (already logical) comes off.
local reserved = reserved_edges(monitor)
local usable_h = monitor.height / monitor.scale - reserved.top - reserved.bottom
local usable_w = nil
if monitor.width then
usable_w = monitor.width / monitor.scale - reserved.left - reserved.right
local reserved = monitor.reserved
height = height / monitor.scale - reserved.top - reserved.bottom
width = width / monitor.scale - reserved.left - reserved.right
local tall = math.floor(height * share)
local wide = width
if alone() then
wide = math.min(width, tall * box)
end
local ratio = box_ratio()
local h = math.max(min_size, math.floor(usable_h * share))
local side = 0
local bottom = math.max(0, math.floor(usable_h - h))
if ratio and usable_w then
local w = math.floor(h * ratio)
if w > usable_w then
w = math.floor(usable_w)
end
side = math.max(0, math.floor((usable_w - w) / 2))
if usable_w - (side * 2) < min_size then
side = math.max(0, math.floor((usable_w - min_size) / 2))
end
end
if usable_h - bottom < min_size then
bottom = math.max(0, math.floor(usable_h - min_size))
end
return cover({ top = 0, right = side, bottom = bottom, left = side })
return cover(math.floor((width - wide) / 2), math.floor(height - tall))
end
local function apply_now()
if hl.exec_scheduled_prop_refresh_immediately then
-- The console keeps the geometry of the output it is open on: a follow_mouse hop
-- onto another screen must not resize a console that is already showing. While
-- it is hidden there is nothing to size but the output that will show it next.
local function console_monitor()
local ws = hl.get_workspace(SCRATCHPAD)
local mon = ws and ws.visible and ws.monitor
if mon and mon.scale and mon.scale > 0 then
return mon
end
return hl.get_active_monitor()
end
local function refit(monitor)
if fit(monitor or console_monitor()) then
-- Land the new gaps in this pass rather than a frame later, so the console
-- does not visibly resize itself once it has already dropped down.
hl.exec_scheduled_prop_refresh_immediately()
end
end
local function scratchpad_on_other_monitor(mon)
if not hl.get_workspace or not mon then
return false
end
local ws = hl.get_workspace(SCRATCHPAD)
if not is_scratchpad(ws) or not ws.visible or not ws.monitor or not ws.monitor.name or not mon.name then
return false
end
return ws.monitor.name ~= mon.name
end
-- Until a monitor can be read, cover the whole work area rather than leaving
-- the console unruled, so it is never seeded without its placement.
cover({ top = 0, right = 0, bottom = 0, left = 0 })
fit()
-- the console unruled, so it is never seeded without its placement. A reload
-- runs this again with the console already on screen, so it starts from the
-- output the console is on rather than whichever one the pointer is over.
cover(0, 0)
fit(console_monitor())
hl.on("monitor.layout_changed", function()
local ws = hl.get_workspace and hl.get_workspace(SCRATCHPAD)
if is_scratchpad(ws) and ws.visible and ws.monitor then
fit(ws.monitor)
else
fit()
end
refit()
end)
-- follow_mouse hops fire this; do not rewrite an open console to a different
-- output's gaps (that is what zeroed the window on the 1080p screen).
hl.on("monitor.focused", function(mon)
if scratchpad_on_other_monitor(mon) then
return
end
fit(mon)
hl.on("monitor.focused", function()
refit()
end)
-- Special workspaces toggle on the monitor they open on, not whichever output
-- last happened to be focused when the rule was written.
-- Special workspaces open on the monitor they are toggled on, not on whichever
-- output last happened to be focused when the rule was written, so these two
-- take the monitor they are handed rather than looking one up.
hl.on("workspace.special_active", function(ws, mon)
if not is_scratchpad(ws) then
return
end
if fit(mon) then
apply_now()
if ws and ws.name == SCRATCHPAD then
refit(mon)
end
end)
hl.on("workspace.move_to_monitor", function(ws, mon)
if not is_scratchpad(ws) then
return
end
if fit(mon) then
apply_now()
if ws and ws.name == SCRATCHPAD then
refit(mon)
end
end)
-- The panel is only centered while the console holds one window, so the count
-- has to be rechecked as apps come and go. These are the two events that run
-- after the workspace's count has already moved: window.close and
-- window.move_to_workspace still count the window on its way out, and refitting
-- from those would read one too many and leave the console full width.
--
-- Only while it is on screen, though. A hidden console is refitted on its way in
-- by workspace.special_active, and every window opened anywhere on the desktop
-- would otherwise rewrite the rule.
local function recount()
local ws = hl.get_workspace(SCRATCHPAD)
if ws and ws.visible then
refit()
end
end
hl.on("window.open", recount)
hl.on("window.destroy", recount)
-- The direction names the edge the offset is measured from, not where the
-- workspace goes: "slide top" drops it down into view, and "slide bottom"
-- retracts it back up the way a Quake console does.
+1 -1
View File
@@ -66,7 +66,7 @@ Finally, there's a special scratchpad workspace that drops down over whatever wo
It works especially well for a terminal running an agent, or for controls you want to interact with quickly without leaving the current workspace. To move a window off the scratchpad, send it directly to another workspace with something like `Super + Shift + 1`.
By default the scratchpad drops down as a centered panel. Open `~/.config/hypr/hyprland.lua` if you want to change how wide that panel is (`omarchy_qconsole_ratio` is near the top). `1` makes a square; `2` makes a panel twice as wide as it is tall; `3` or `4` is wider still. Anything smaller than `1` still gives you a square. Comment the line out if you'd rather have it span the full width of the screen.
While the scratchpad holds a single window, it drops down as a centered panel rather than spanning the screen. Put a second app on it and it goes back to the full width, so the two have room to sit side by side.
### It takes some getting used to!
+128 -58
View File
@@ -4,13 +4,13 @@ source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
require_command lua
# The console is sized by the gap underneath it, recomputed from the monitor,
# 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 half a screen on a
# scaled display, so it is worth pinning down.
# 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 half-height 2:1 panel by default"
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
@@ -25,10 +25,6 @@ hl = {
exec_scheduled_prop_refresh_immediately = function() end,
}
-- Same default as config/hypr/hyprland.lua: a centered panel twice as wide as
-- it is tall. Commenting that assignment out is what restores full width.
omarchy_qconsole_ratio = 2
dofile(os.getenv("OMARCHY_PATH") .. "/default/hypr/bootstrap.lua")
require("default.hypr.qconsole")
@@ -50,13 +46,12 @@ assert(current().on_created_empty:find("^%[workspace special:scratchpad silent%]
assert(current().workspace == "special:scratchpad")
local function rescale(height, scale, bar)
monitor = { height = height, scale = scale, reserved = { top = bar, bottom = 0, left = 0, right = 0 } }
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.
-- These fixtures omit width, so the ratio cannot inset the sides yet.
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")
@@ -66,9 +61,8 @@ assert(rescale(2160, 1.5, 40) == 700, "and at a fractional scale")
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")
assert(final.gaps_out.left == 0 and final.gaps_out.right == 0,
"without a width the sides stay flush")
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")
@@ -81,86 +75,162 @@ assert(current().gaps_out.bottom == before, "an absent monitor leaves the consol
-- 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.
monitor = setmetatable({}, { __index = function() return nil end })
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 = { height = 1440, scale = 1, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
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")
monitor.scale = 2
handlers["monitor.layout_changed"]()
assert(#rules == written + 1, "a real change still rewrites it")
assert(current().gaps_out.bottom == 360, "and lands on half the rescaled screen")
-- Default ratio: 16:9 is a centered 2:1 panel, not a full-width drop-down.
monitor = { width = 1920, height = 1080, scale = 1, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
-- 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 default is a 1080x540 panel")
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, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
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).
-- It checks that scale does not change the panel's logical size.
monitor = { name = "DP-1", width = 12288, height = 5120, scale = 2, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
-- 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")
-- Acer 1920x1080, bar 30, same ratio. Opening here after a 6K fit must rewrite;
-- leaving 6K side gaps would make leftover width negative.
local acer = { name = "HDMI-A-1", width = 1920, height = 1080, scale = 1, reserved = { top = 30, bottom = 0, left = 0, right = 0 } }
-- 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 2:1 box")
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 global rule (that is what oversized the Dell after a hop).
workspace = { name = "special:scratchpad", visible = true, monitor = acer }
-- 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")
workspace = nil
-- Cache: same 1440p height, 16:9 vs 21:9. Sides change even when bottom does not.
monitor = { width = 2560, height = 1440, scale = 1, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
written = #rules
monitor = { width = 3440, height = 1440, scale = 1, 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 at ratio 2 is a 2:1 box")
omarchy_qconsole_ratio = 1
monitor = { width = 1920, height = 1080, scale = 1, reserved = { top = 0, bottom = 0, left = 0, right = 0 } }
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 690 and right == 690 and bottom == 540, "ratio 1 is a square")
omarchy_qconsole_ratio = 0.5
handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps()
assert(left == 690 and right == 690 and bottom == 540, "a ratio below 1 clamps to a square")
omarchy_qconsole_ratio = nil
-- 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 == 0 and right == 0 and bottom == 1265, "clearing the ratio restores full width")
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")
-- window.close and window.move_to_workspace both run while the workspace still
-- counts the window that is leaving, so a refit from either reads one too many
-- and strands the console at full width. window.open and window.destroy are the
-- two that run after the count has already moved, and are the only ones hooked.
assert(handlers["window.close"] == nil, "window.close counts the window on its way out")
assert(handlers["window.move_to_workspace"] == nil, "window.move_to_workspace does too")
LUA
pass "the console is a half-height 2:1 panel by default"
pass "the console is a centered panel until a second app joins it"