Size it from leftover workspace gaps so it refits per monitor, and let omarchy_qconsole_ratio change the width.
214 lines
6.8 KiB
Lua
214 lines
6.8 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
|
|
local min_size = 64
|
|
|
|
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,
|
|
},
|
|
})
|
|
|
|
-- 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 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
|
|
return false
|
|
end
|
|
covering = gaps_out
|
|
|
|
hl.workspace_rule({
|
|
workspace = SCRATCHPAD,
|
|
gaps_in = 0,
|
|
gaps_out = gaps_out,
|
|
|
|
-- 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
|
|
|
|
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")
|
|
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.
|
|
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.
|
|
if not monitor or not monitor.scale or monitor.scale <= 0 then
|
|
return false
|
|
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
|
|
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 })
|
|
end
|
|
|
|
local function apply_now()
|
|
if hl.exec_scheduled_prop_refresh_immediately then
|
|
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()
|
|
|
|
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
|
|
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)
|
|
end)
|
|
|
|
-- Special workspaces toggle on the monitor they open on, not whichever output
|
|
-- last happened to be focused when the rule was written.
|
|
hl.on("workspace.special_active", function(ws, mon)
|
|
if not is_scratchpad(ws) then
|
|
return
|
|
end
|
|
if fit(mon) then
|
|
apply_now()
|
|
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()
|
|
end
|
|
end)
|
|
|
|
-- 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" })
|