diff --git a/default/hypr/qconsole.lua b/default/hypr/qconsole.lua index cb248f4c..a512f8ae 100644 --- a/default/hypr/qconsole.lua +++ b/default/hypr/qconsole.lua @@ -63,10 +63,17 @@ local function cover(side, bottom) end -- 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 ws = hl.get_workspace(SCRATCHPAD) - return not ws or ws.windows <= 1 + local tiled = 0 + for _, window in ipairs(hl.get_workspace_windows(SCRATCHPAD)) do + if not window.floating then + tiled = tiled + 1 + end + end + return tiled <= 1 end -- 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) --- 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. +-- The panel is only centered while the console holds one tiled window, so the +-- count has to be rechecked as apps come and go: opened and closed, moved on or +-- off (Super+Alt+S, Super+Shift+1), and floated or tiled (Super+T, Super+O). +-- window.close is left out, since it still counts the window on its way out and +-- 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 -- 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.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 -- workspace goes: "slide top" drops it down into view, and "slide bottom" diff --git a/test/shell.d/hyprland-qconsole-test.sh b/test/shell.d/hyprland-qconsole-test.sh index c5123d03..57feb176 100755 --- a/test/shell.d/hyprland-qconsole-test.sh +++ b/test/shell.d/hyprland-qconsole-test.sh @@ -22,6 +22,14 @@ hl = { 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, } @@ -226,11 +234,46 @@ 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. +-- 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") -assert(handlers["window.move_to_workspace"] == nil, "window.move_to_workspace does too") LUA pass "the console is a centered panel until a second app joins it"