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
197 lines
7.6 KiB
Lua
197 lines
7.6 KiB
Lua
-- The scratchpad, presented as a Quake console: a dimmed overlay that drops
|
|
-- down over whatever workspace you are on. Its bindings live in
|
|
-- bindings/tiling.lua, and its slide is animated below.
|
|
|
|
-- How much of the usable screen the console covers, measured from the top.
|
|
local share = 0.5
|
|
|
|
-- 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"
|
|
|
|
-- Seed the console with the default agent the first time it opens, rather than
|
|
-- at boot, so nothing is running until it is wanted. The exec rule has to pin
|
|
-- the workspace itself: Hyprland only tags a spawn with the workspace it came
|
|
-- from while misc.initial_workspace_tracking is on, and looknfeel turns it off.
|
|
-- Omarchy ships without a default agent, and omarchy-agent exits without
|
|
-- opening anything when none is set, so until one is picked this just opens an
|
|
-- empty console.
|
|
local seed = "[workspace special:scratchpad silent] omarchy-agent"
|
|
|
|
-- Dimming only applies while a special workspace is open, so the console gets
|
|
-- its separation from the workspace underneath without costing anything the
|
|
-- rest of the time.
|
|
hl.config({
|
|
decoration = {
|
|
dim_special = 0.6,
|
|
},
|
|
})
|
|
|
|
-- 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 beside, below = nil, nil
|
|
|
|
local function cover(side, bottom)
|
|
if beside == side and below == bottom then
|
|
return false
|
|
end
|
|
beside, below = side, bottom
|
|
|
|
hl.workspace_rule({
|
|
workspace = SCRATCHPAD,
|
|
gaps_in = 0,
|
|
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
|
|
-- is already set apart by the dimming behind it.
|
|
no_border = true,
|
|
|
|
on_created_empty = seed,
|
|
})
|
|
|
|
return true
|
|
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. 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 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
|
|
-- 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 area left around it, and that area is
|
|
-- recomputed whenever the monitor it opens on changes.
|
|
local function fit(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
|
|
-- 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 = 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
|
|
|
|
return cover(math.floor((width - wide) / 2), math.floor(height - tall))
|
|
end
|
|
|
|
-- 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
|
|
|
|
-- 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. 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()
|
|
refit()
|
|
end)
|
|
|
|
hl.on("monitor.focused", function()
|
|
refit()
|
|
end)
|
|
|
|
-- 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 ws and ws.name == SCRATCHPAD then
|
|
refit(mon)
|
|
end
|
|
end)
|
|
|
|
hl.on("workspace.move_to_monitor", function(ws, mon)
|
|
if ws and ws.name == SCRATCHPAD then
|
|
refit(mon)
|
|
end
|
|
end)
|
|
|
|
-- 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
|
|
-- 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)
|
|
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"
|
|
-- retracts it back up the way a Quake console does.
|
|
hl.animation({ leaf = "specialWorkspaceIn", enabled = true, speed = 3, bezier = "easeOutQuint", style = "slide top" })
|
|
hl.animation({ leaf = "specialWorkspaceOut", enabled = true, speed = 2, bezier = "easeInOutCubic", style = "slide bottom" })
|