Make scratchpad exclusivity stateful

This commit is contained in:
2026-07-11 15:42:05 -07:00
parent 5b31007da2
commit 534196af94
3 changed files with 66 additions and 18 deletions

View File

@@ -260,8 +260,15 @@ Required behavior:
- A named scratchpad exists for volume.
- A named scratchpad exists for x.com.
- Scratchpads appear near-fullscreen and centered by default.
- Regular scratchpads can be tiled into the normal workspace when desired,
while retaining their summon/dismiss toggles.
- Floating scratchpad behavior is a state of a window, not a property of its
application class. Floating any window at scratchpad geometry enters that
state; tiling it leaves that state.
- Invoking a scratchpad replaces only another window currently in floating
scratchpad mode. Other floating windows are left alone.
- Regular scratchpad targets can be tiled into the normal workspace when
desired, while retaining their shortcut association.
- Summoning a scratchpad target with its shortcut always restores floating
scratchpad mode, even if the window was previously tiled.
- Toggling a scratchpad deactivates fullscreen/tabbed state first.
- Floating scratchpads are hidden from normal workspace and window listings.

View File

@@ -8,6 +8,7 @@ function M.setup(ctx)
dropdown_animation_frames = 18
dropdown_animation_frame_ms = 16
scratchpad_pending = {}
scratchpad_mode_tag = "scratchpad"
monitor_reserved_cache_path = (os.getenv("XDG_RUNTIME_DIR") or "/tmp") .. "/hyprland-monitor-reserved.tsv"
scratchpad_fallback_reserved_top = 60
@@ -100,15 +101,23 @@ function M.setup(ctx)
return def.allow_tiling ~= false and window and window.floating == false
end
local function is_scratchpad_window(window)
for _, def in pairs(scratchpads) do
if scratchpad_window_matches(window, def) and not tiled_scratchpad_is_normal_window(window, def) then
local function window_has_scratchpad_tag(window)
for _, tag in ipairs((window and window.tags) or {}) do
if tag == scratchpad_mode_tag then
return true
end
end
return false
end
local function window_is_floating_scratchpad(window)
return window and window.floating == true and window_has_scratchpad_tag(window)
end
local function is_scratchpad_window(window)
return window_is_floating_scratchpad(window)
end
local function matching_scratchpad_name(window)
for name, def in pairs(scratchpads) do
if scratchpad_window_matches(window, def) then
@@ -122,6 +131,31 @@ function M.setup(ctx)
return "name:scratch-hidden-" .. name
end
local function mark_window_as_floating_scratchpad(window, name)
local selector = window_selector(window)
if not selector then
return
end
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
dispatch(hl.dsp.window.tag({ tag = "+" .. scratchpad_mode_tag, window = selector }))
if name then
dispatch(hl.dsp.window.tag({ tag = "+scratchpad-" .. name, window = selector }))
end
end
local function clear_floating_scratchpad_state(window)
local selector = window_selector(window)
if not selector then
return
end
dispatch(hl.dsp.window.tag({ tag = "-" .. scratchpad_mode_tag, window = selector }))
for name in pairs(scratchpads) do
dispatch(hl.dsp.window.tag({ tag = "-scratchpad-" .. name, window = selector }))
end
end
local function as_number(value, default)
local number = tonumber(value)
if number == nil then
@@ -337,9 +371,7 @@ function M.setup(ctx)
end
local selector = window_selector(window)
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
dispatch(hl.dsp.window.tag({ tag = "+scratchpad", window = selector }))
dispatch(hl.dsp.window.tag({ tag = "+scratchpad-" .. name, window = selector }))
mark_window_as_floating_scratchpad(window, name)
dispatch(hl.dsp.window.resize({ x = geometry.width, y = geometry.height, relative = false, window = selector }))
dispatch(hl.dsp.window.move({ x = geometry.x, y = geometry.y, relative = false, window = selector }))
if def.dropdown then
@@ -382,7 +414,8 @@ function M.setup(ctx)
local function hide_scratchpad_window(name, window)
remove_minimized_window(window)
move_window_to_workspace(scratchpad_workspace(name), false, window)
name = name or matching_scratchpad_name(window)
move_window_to_workspace(name and scratchpad_workspace(name) or "special:NSP", false, window)
end
local function scratchpad_show_workspace(workspace)
@@ -401,6 +434,7 @@ function M.setup(ctx)
end
remove_minimized_window(window)
mark_window_as_floating_scratchpad(window, name)
if scratchpads[name] and scratchpads[name].dropdown then
apply_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor(), "above")
end
@@ -408,8 +442,10 @@ function M.setup(ctx)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
if scratchpads[name] and scratchpads[name].dropdown then
animate_dropdown_scratchpad_down(name, window, target_monitor or hl.get_active_monitor())
elseif should_apply_scratchpad_geometry(name, window, opts) then
schedule_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor(), nil, nil, opts)
else
schedule_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor(), nil, nil, {
force_geometry = true,
})
end
end
@@ -418,17 +454,17 @@ function M.setup(ctx)
return workspace and window and same_workspace(window.workspace, workspace)
end
-- Active scratchpads are scratchpad windows visible on the active workspace.
-- Invoking a different scratchpad replaces that active set.
-- Scratchpad exclusivity belongs to a window's current presentation state,
-- not its class. Invoking a named shortcut replaces any other window that is
-- visibly in floating scratchpad mode, including an otherwise ordinary app.
local function active_scratchpad_windows(except_name)
local windows = {}
for _, window in ipairs(hl.get_windows()) do
local name = matching_scratchpad_name(window)
if
name
and name ~= except_name
(except_name == nil or name ~= except_name)
and scratchpad_is_visible(window)
and not tiled_scratchpad_is_normal_window(window, scratchpads[name])
and window_is_floating_scratchpad(window)
then
windows[#windows + 1] = {
name = name,
@@ -474,7 +510,7 @@ function M.setup(ctx)
show_scratchpad_window(name, window, pending.workspace or active_workspace(), pending.monitor or hl.get_active_monitor(), {
force_geometry = true,
})
elseif scratchpad_is_visible(window) then
elseif scratchpad_is_visible(window) and window_is_floating_scratchpad(window) then
schedule_scratchpad_geometry(name, window, hl.get_active_monitor())
end
end
@@ -584,9 +620,13 @@ function M.setup(ctx)
ctx.lower_contains_any = lower_contains_any
ctx.scratchpad_window_matches = scratchpad_window_matches
ctx.tiled_scratchpad_is_normal_window = tiled_scratchpad_is_normal_window
ctx.window_has_scratchpad_tag = window_has_scratchpad_tag
ctx.window_is_floating_scratchpad = window_is_floating_scratchpad
ctx.is_scratchpad_window = is_scratchpad_window
ctx.matching_scratchpad_name = matching_scratchpad_name
ctx.scratchpad_workspace = scratchpad_workspace
ctx.mark_window_as_floating_scratchpad = mark_window_as_floating_scratchpad
ctx.clear_floating_scratchpad_state = clear_floating_scratchpad_state
ctx.as_number = as_number
ctx.logical_monitor_dimension = logical_monitor_dimension
ctx.split_tsv = split_tsv

View File

@@ -140,7 +140,7 @@ function M.setup(ctx)
action = "set",
window = selector,
}))
dispatch(hl.dsp.window.float({ action = "enable", window = selector }))
mark_window_as_floating_scratchpad(window, matching_scratchpad_name(window))
dispatch(hl.dsp.window.resize({ x = geometry.width, y = geometry.height, relative = false, window = selector }))
dispatch(hl.dsp.window.move({ x = geometry.x, y = geometry.y, relative = false, window = selector }))
end
@@ -153,6 +153,7 @@ function M.setup(ctx)
end
if window.floating then
clear_floating_scratchpad_state(window)
dispatch(hl.dsp.window.float({ action = "disable", window = selector }))
return
end