diff --git a/config/hypr/hyprland.lua b/config/hypr/hyprland.lua index 354e079c..45c9b664 100644 --- a/config/hypr/hyprland.lua +++ b/config/hypr/hyprland.lua @@ -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") diff --git a/default/hypr/qconsole.lua b/default/hypr/qconsole.lua index 235b4675..cb248f4c 100644 --- a/default/hypr/qconsole.lua +++ b/default/hypr/qconsole.lua @@ -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. diff --git a/manual/04-navigation.md b/manual/04-navigation.md index 1b62b0df..0b3404ff 100644 --- a/manual/04-navigation.md +++ b/manual/04-navigation.md @@ -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! diff --git a/test/shell.d/hyprland-qconsole-test.sh b/test/shell.d/hyprland-qconsole-test.sh index 9360e3c8..c5123d03 100755 --- a/test/shell.d/hyprland-qconsole-test.sh +++ b/test/shell.d/hyprland-qconsole-test.sh @@ -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"