Count only tiled windows and refit when apps move or float

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
This commit is contained in:
David Heinemeier HanssonandClaude Opus 5 committed 2026-09-13 12:37:08 +02:00
1 parent a59afb9659
commit d377569edb
2 files changed
+68 -13

No files matched your search

+20 -8
View File
@@ -63,10 +63,17 @@ local function cover(side, bottom)
end end
-- One window reads as a console and gets the panel. A second app has turned the -- 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. -- scratchpad into a workspace, and a workspace wants the whole width. Only tiled
-- windows count: the gaps are what size the panel, and a floating window on top
-- of the console is not laid out by them.
local function alone() local function alone()
local ws = hl.get_workspace(SCRATCHPAD) local tiled = 0
return not ws or ws.windows <= 1 for _, window in ipairs(hl.get_workspace_windows(SCRATCHPAD)) do
if not window.floating then
tiled = tiled + 1
end
end
return tiled <= 1
end end
-- Sizing the console with a window rule would freeze it at whatever the screen -- Sizing the console with a window rule would freeze it at whatever the screen
@@ -158,11 +165,14 @@ hl.on("workspace.move_to_monitor", function(ws, mon)
end end
end) end)
-- The panel is only centered while the console holds one window, so the count -- The panel is only centered while the console holds one tiled window, so the
-- has to be rechecked as apps come and go. These are the two events that run -- count has to be rechecked as apps come and go: opened and closed, moved on or
-- after the workspace's count has already moved: window.close and -- off (Super+Alt+S, Super+Shift+1), and floated or tiled (Super+T, Super+O).
-- window.move_to_workspace still count the window on its way out, and refitting -- window.close is left out, since it still counts the window on its way out and
-- from those would read one too many and leave the console full width. -- window.destroy follows it anyway. Measured on Hyprland 0.56.2, a move is
-- trailed by several window.update_rules, as is a float toggle, and the last of
-- those always reads the settled count; the earlier ones refit to what the rule
-- already is, which cover() skips.
-- --
-- Only while it is on screen, though. A hidden console is refitted on its way in -- 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 -- by workspace.special_active, and every window opened anywhere on the desktop
@@ -176,6 +186,8 @@ end
hl.on("window.open", recount) hl.on("window.open", recount)
hl.on("window.destroy", recount) hl.on("window.destroy", recount)
hl.on("window.move_to_workspace", recount)
hl.on("window.update_rules", recount)
-- The direction names the edge the offset is measured from, not where the -- 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" -- workspace goes: "slide top" drops it down into view, and "slide bottom"
+48 -5
View File
@@ -22,6 +22,14 @@ hl = {
on = function(event, callback) handlers[event] = callback end, on = function(event, callback) handlers[event] = callback end,
get_active_monitor = function() return monitor end, get_active_monitor = function() return monitor end,
get_workspace = function() return workspace 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, exec_scheduled_prop_refresh_immediately = function() end,
} }
@@ -226,11 +234,46 @@ handlers["monitor.layout_changed"]()
top, right, bottom, left = gaps() 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") 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 -- Back to a console on screen for the move and float checks.
-- counts the window that is leaving, so a refit from either reads one too many workspace = { name = "special:scratchpad", visible = true, monitor = acer, windows = 1 }
-- and strands the console at full width. window.open and window.destroy are the monitor = acer
-- two that run after the count has already moved, and are the only ones hooked. 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") 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 LUA
pass "the console is a centered panel until a second app joins it" pass "the console is a centered panel until a second app joins it"