Extract Hyprland scratchpad module
This commit is contained in:
@@ -21,6 +21,7 @@ package.path = table.concat({
|
|||||||
|
|
||||||
local modules = {
|
local modules = {
|
||||||
"hyprland.state",
|
"hyprland.state",
|
||||||
|
"hyprland.scratchpads",
|
||||||
"hyprland.core",
|
"hyprland.core",
|
||||||
"hyprland.layouts",
|
"hyprland.layouts",
|
||||||
"hyprland.windows",
|
"hyprland.windows",
|
||||||
|
|||||||
@@ -275,53 +275,6 @@ function M.setup(ctx)
|
|||||||
return workspace and not workspace.special and workspace.id and workspace.id >= 1
|
return workspace and not workspace.special and workspace.id and workspace.id >= 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local function lower_contains(value, needle)
|
|
||||||
if not needle or needle == "" then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
value = string.lower(tostring(value or ""))
|
|
||||||
needle = string.lower(tostring(needle))
|
|
||||||
return value:find(needle, 1, true) ~= nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function lower_contains_any(value, needles)
|
|
||||||
if type(needles) ~= "table" then
|
|
||||||
return lower_contains(value, needles)
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, needle in ipairs(needles) do
|
|
||||||
if lower_contains(value, needle) then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function scratchpad_window_matches(window, def)
|
|
||||||
return window
|
|
||||||
and lower_contains_any(window.class, def.classes or def.class)
|
|
||||||
and lower_contains(window.title, def.title)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function is_scratchpad_window(window)
|
|
||||||
for _, def in pairs(scratchpads) do
|
|
||||||
if scratchpad_window_matches(window, def) then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
local function matching_scratchpad_name(window)
|
|
||||||
for name, def in pairs(scratchpads) do
|
|
||||||
if scratchpad_window_matches(window, def) then
|
|
||||||
return name
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function same_workspace(left, right)
|
local function same_workspace(left, right)
|
||||||
if not left or not right then
|
if not left or not right then
|
||||||
return false
|
return false
|
||||||
@@ -554,11 +507,6 @@ function M.setup(ctx)
|
|||||||
ctx.current_workspace_layout = current_workspace_layout
|
ctx.current_workspace_layout = current_workspace_layout
|
||||||
ctx.write_layout_state = write_layout_state
|
ctx.write_layout_state = write_layout_state
|
||||||
ctx.is_normal_workspace = is_normal_workspace
|
ctx.is_normal_workspace = is_normal_workspace
|
||||||
ctx.lower_contains = lower_contains
|
|
||||||
ctx.lower_contains_any = lower_contains_any
|
|
||||||
ctx.scratchpad_window_matches = scratchpad_window_matches
|
|
||||||
ctx.is_scratchpad_window = is_scratchpad_window
|
|
||||||
ctx.matching_scratchpad_name = matching_scratchpad_name
|
|
||||||
ctx.same_workspace = same_workspace
|
ctx.same_workspace = same_workspace
|
||||||
ctx.is_minimized_workspace = is_minimized_workspace
|
ctx.is_minimized_workspace = is_minimized_workspace
|
||||||
ctx.is_minimized_window = is_minimized_window
|
ctx.is_minimized_window = is_minimized_window
|
||||||
|
|||||||
430
dotfiles/config/hypr/hyprland/scratchpads.lua
Normal file
430
dotfiles/config/hypr/hyprland/scratchpads.lua
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
local M = {}
|
||||||
|
|
||||||
|
function M.setup(ctx)
|
||||||
|
local _ENV = ctx
|
||||||
|
|
||||||
|
scratchpad_size_ratio = 0.95
|
||||||
|
dropdown_height_ratio = 0.5
|
||||||
|
scratchpad_pending = {}
|
||||||
|
monitor_reserved_cache_path = (os.getenv("XDG_RUNTIME_DIR") or "/tmp") .. "/hyprland-monitor-reserved.tsv"
|
||||||
|
scratchpad_fallback_reserved_top = 60
|
||||||
|
|
||||||
|
scratchpads = {
|
||||||
|
codex = {
|
||||||
|
command = "codex-desktop",
|
||||||
|
class = "codex-desktop",
|
||||||
|
},
|
||||||
|
htop = {
|
||||||
|
command = "alacritty --class htop-scratch --title htop -e htop",
|
||||||
|
class = "htop-scratch",
|
||||||
|
},
|
||||||
|
volume = {
|
||||||
|
command = "pavucontrol",
|
||||||
|
class = "org.pulseaudio.pavucontrol",
|
||||||
|
},
|
||||||
|
spotify = {
|
||||||
|
command = "spotify",
|
||||||
|
class = "spotify",
|
||||||
|
},
|
||||||
|
element = {
|
||||||
|
command = "element-desktop",
|
||||||
|
classes = { "Element", "electron" },
|
||||||
|
title = "Element",
|
||||||
|
},
|
||||||
|
slack = {
|
||||||
|
command = "slack",
|
||||||
|
class = "Slack",
|
||||||
|
},
|
||||||
|
messages = {
|
||||||
|
command = "google-chrome-stable --profile-directory=Default --app=https://messages.google.com/web/conversations",
|
||||||
|
class = "chrome-messages.google.com",
|
||||||
|
},
|
||||||
|
transmission = {
|
||||||
|
command = "transmission-gtk",
|
||||||
|
class = "transmission-gtk",
|
||||||
|
},
|
||||||
|
dropdown = {
|
||||||
|
command = "ghostty --config-file=/home/imalison/.config/ghostty/dropdown",
|
||||||
|
class = "com.mitchellh.ghostty.dropdown",
|
||||||
|
dropdown = true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local function lower_contains(value, needle)
|
||||||
|
if not needle or needle == "" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
value = string.lower(tostring(value or ""))
|
||||||
|
needle = string.lower(tostring(needle))
|
||||||
|
return value:find(needle, 1, true) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function lower_contains_any(value, needles)
|
||||||
|
if type(needles) ~= "table" then
|
||||||
|
return lower_contains(value, needles)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, needle in ipairs(needles) do
|
||||||
|
if lower_contains(value, needle) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function scratchpad_window_matches(window, def)
|
||||||
|
return window
|
||||||
|
and lower_contains_any(window.class, def.classes or def.class)
|
||||||
|
and lower_contains(window.title, def.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_scratchpad_window(window)
|
||||||
|
for _, def in pairs(scratchpads) do
|
||||||
|
if scratchpad_window_matches(window, def) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function matching_scratchpad_name(window)
|
||||||
|
for name, def in pairs(scratchpads) do
|
||||||
|
if scratchpad_window_matches(window, def) then
|
||||||
|
return name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function scratchpad_workspace(name)
|
||||||
|
return "special:scratch-" .. name
|
||||||
|
end
|
||||||
|
|
||||||
|
local function as_number(value, default)
|
||||||
|
local number = tonumber(value)
|
||||||
|
if number == nil then
|
||||||
|
return default
|
||||||
|
end
|
||||||
|
return number
|
||||||
|
end
|
||||||
|
|
||||||
|
local function logical_monitor_dimension(value, scale)
|
||||||
|
value = as_number(value, 0)
|
||||||
|
scale = as_number(scale, 1)
|
||||||
|
if scale <= 0 then
|
||||||
|
scale = 1
|
||||||
|
end
|
||||||
|
return math.floor((value / scale) + 0.5)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function split_tsv(line)
|
||||||
|
local fields = {}
|
||||||
|
for field in (line .. "\t"):gmatch("([^\t]*)\t") do
|
||||||
|
fields[#fields + 1] = field
|
||||||
|
end
|
||||||
|
return fields
|
||||||
|
end
|
||||||
|
|
||||||
|
local function monitor_from_reserved_fields(monitor, fields)
|
||||||
|
if not monitor or not monitor.name or fields[1] ~= monitor.name or #fields < 10 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
name = monitor.name,
|
||||||
|
x = tonumber(fields[2]),
|
||||||
|
y = tonumber(fields[3]),
|
||||||
|
width = tonumber(fields[4]),
|
||||||
|
height = tonumber(fields[5]),
|
||||||
|
scale = tonumber(fields[6]),
|
||||||
|
reserved = {
|
||||||
|
tonumber(fields[7]),
|
||||||
|
tonumber(fields[8]),
|
||||||
|
tonumber(fields[9]),
|
||||||
|
tonumber(fields[10]),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function monitor_from_reserved_lines(monitor, lines)
|
||||||
|
if not monitor or not monitor.name then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
for line in lines do
|
||||||
|
local cached = monitor_from_reserved_fields(monitor, split_tsv(line))
|
||||||
|
if cached then
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function monitor_from_reserved_cache(monitor)
|
||||||
|
if verify_config or not monitor or not monitor.name then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local file = io.open(monitor_reserved_cache_path, "r")
|
||||||
|
if not file then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local cached = monitor_from_reserved_lines(monitor, file:lines())
|
||||||
|
file:close()
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
|
local function refresh_monitor_reserved_cache(delay)
|
||||||
|
if verify_config then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local command = string.format(
|
||||||
|
[=[sleep %.2f; cache="${XDG_RUNTIME_DIR:-/tmp}/hyprland-monitor-reserved.tsv"; tmp="$cache.tmp"; /run/current-system/sw/bin/hyprctl -j monitors 2>/dev/null | /run/current-system/sw/bin/jq -r '.[] | [.name, .x, .y, .width, .height, .scale, .reserved[0], .reserved[1], .reserved[2], .reserved[3]] | @tsv' > "$tmp" && mv "$tmp" "$cache"]=],
|
||||||
|
as_number(delay, 0)
|
||||||
|
)
|
||||||
|
hl.exec_cmd("sh -lc " .. shell_quote(command))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function monitor_workarea(monitor)
|
||||||
|
monitor = monitor_from_reserved_cache(monitor) or monitor
|
||||||
|
local width = logical_monitor_dimension(monitor.width, monitor.scale)
|
||||||
|
local height = logical_monitor_dimension(monitor.height, monitor.scale)
|
||||||
|
local reserved = monitor.reserved or { 0, scratchpad_fallback_reserved_top, 0, 0 }
|
||||||
|
local left = math.floor(as_number(reserved[1], 0))
|
||||||
|
local top = math.floor(as_number(reserved[2], 0))
|
||||||
|
local right = math.floor(as_number(reserved[3], 0))
|
||||||
|
local bottom = math.floor(as_number(reserved[4], 0))
|
||||||
|
local work_width = width - left - right
|
||||||
|
local work_height = height - top - bottom
|
||||||
|
|
||||||
|
if work_width <= 0 then
|
||||||
|
left = 0
|
||||||
|
right = 0
|
||||||
|
work_width = width
|
||||||
|
end
|
||||||
|
if work_height <= 0 then
|
||||||
|
top = 0
|
||||||
|
bottom = 0
|
||||||
|
work_height = height
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
x = math.floor(as_number(monitor.x, 0)) + left,
|
||||||
|
y = math.floor(as_number(monitor.y, 0)) + top,
|
||||||
|
width = work_width,
|
||||||
|
height = work_height,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function matching_scratchpad_windows(name)
|
||||||
|
local def = scratchpads[name]
|
||||||
|
local windows = {}
|
||||||
|
if not def then
|
||||||
|
return windows
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, window in ipairs(hl.get_windows()) do
|
||||||
|
if scratchpad_window_matches(window, def) then
|
||||||
|
windows[#windows + 1] = window
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return windows
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply_scratchpad_geometry(name, window, target_monitor)
|
||||||
|
local def = scratchpads[name]
|
||||||
|
local monitor = target_monitor or hl.get_active_monitor()
|
||||||
|
if not def or not window or not monitor then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local workarea = monitor_workarea(monitor)
|
||||||
|
local width
|
||||||
|
local height
|
||||||
|
local x
|
||||||
|
local y
|
||||||
|
if def.dropdown then
|
||||||
|
width = workarea.width
|
||||||
|
height = math.floor(workarea.height * dropdown_height_ratio)
|
||||||
|
x = workarea.x
|
||||||
|
y = workarea.y
|
||||||
|
else
|
||||||
|
width = math.floor(workarea.width * scratchpad_size_ratio)
|
||||||
|
height = math.floor(workarea.height * scratchpad_size_ratio)
|
||||||
|
x = workarea.x + math.floor((workarea.width - width) / 2)
|
||||||
|
y = workarea.y + math.floor((workarea.height - height) / 2)
|
||||||
|
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 }))
|
||||||
|
dispatch(hl.dsp.window.resize({ x = width, y = height, relative = false, window = selector }))
|
||||||
|
dispatch(hl.dsp.window.move({ x = x, y = y, relative = false, window = selector }))
|
||||||
|
if def.dropdown then
|
||||||
|
dispatch(hl.dsp.window.set_prop({ prop = "border_size", value = "0", window = selector }))
|
||||||
|
dispatch(hl.dsp.window.set_prop({ prop = "no_shadow", value = "1", window = selector }))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function schedule_scratchpad_geometry(name, window, target_monitor)
|
||||||
|
hl.timer(function()
|
||||||
|
apply_scratchpad_geometry(name, window, target_monitor)
|
||||||
|
end, { timeout = 50, type = "oneshot" })
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hide_scratchpad_window(name, window)
|
||||||
|
remove_minimized_window(window)
|
||||||
|
move_window_to_workspace(scratchpad_workspace(name), false, window)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function show_scratchpad_window(name, window, workspace, target_monitor)
|
||||||
|
workspace = workspace or active_workspace()
|
||||||
|
if not workspace then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
remove_minimized_window(window)
|
||||||
|
move_window_to_workspace(workspace.id, false, window)
|
||||||
|
dispatch(hl.dsp.focus({ window = window_selector(window) }))
|
||||||
|
schedule_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor())
|
||||||
|
end
|
||||||
|
|
||||||
|
local function scratchpad_is_visible(window)
|
||||||
|
local workspace = active_workspace()
|
||||||
|
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.
|
||||||
|
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 and scratchpad_is_visible(window) then
|
||||||
|
windows[#windows + 1] = {
|
||||||
|
name = name,
|
||||||
|
window = window,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return windows
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hide_active_scratchpads(except_name)
|
||||||
|
for _, active in ipairs(active_scratchpad_windows(except_name)) do
|
||||||
|
hide_scratchpad_window(active.name, active.window)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function refresh_active_scratchpad_geometries()
|
||||||
|
local monitor = hl.get_active_monitor()
|
||||||
|
for _, active in ipairs(active_scratchpad_windows()) do
|
||||||
|
schedule_scratchpad_geometry(active.name, active.window, monitor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function refresh_active_scratchpad_geometries_later(timeout)
|
||||||
|
hl.timer(refresh_active_scratchpad_geometries, { timeout = timeout or 300, type = "oneshot" })
|
||||||
|
end
|
||||||
|
|
||||||
|
local function refresh_shell_workarea_and_scratchpads()
|
||||||
|
refresh_monitor_reserved_cache(0.15)
|
||||||
|
refresh_active_scratchpad_geometries_later(400)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function adopt_matching_scratchpad_window(window)
|
||||||
|
if not window then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
for name, def in pairs(scratchpads) do
|
||||||
|
if scratchpad_window_matches(window, def) then
|
||||||
|
if scratchpad_pending[name] then
|
||||||
|
local pending = scratchpad_pending[name]
|
||||||
|
scratchpad_pending[name] = nil
|
||||||
|
show_scratchpad_window(name, window, pending.workspace or active_workspace(), pending.monitor or hl.get_active_monitor())
|
||||||
|
elseif scratchpad_is_visible(window) then
|
||||||
|
schedule_scratchpad_geometry(name, window, hl.get_active_monitor())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function toggle_scratchpad(name)
|
||||||
|
local def = scratchpads[name]
|
||||||
|
if not def then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_layout == monocle_layout then
|
||||||
|
set_layout(columns_layout)
|
||||||
|
end
|
||||||
|
|
||||||
|
local windows = matching_scratchpad_windows(name)
|
||||||
|
if #windows == 0 then
|
||||||
|
hide_active_scratchpads(name)
|
||||||
|
scratchpad_pending[name] = {
|
||||||
|
monitor = hl.get_active_monitor(),
|
||||||
|
workspace = active_workspace(),
|
||||||
|
}
|
||||||
|
hl.exec_cmd(def.command)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local any_visible = false
|
||||||
|
for _, window in ipairs(windows) do
|
||||||
|
if scratchpad_is_visible(window) then
|
||||||
|
any_visible = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if any_visible then
|
||||||
|
for _, window in ipairs(windows) do
|
||||||
|
hide_scratchpad_window(name, window)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
hide_active_scratchpads(name)
|
||||||
|
local workspace = active_workspace()
|
||||||
|
local target_monitor = hl.get_active_monitor()
|
||||||
|
for _, window in ipairs(windows) do
|
||||||
|
show_scratchpad_window(name, window, workspace, target_monitor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ctx.lower_contains = lower_contains
|
||||||
|
ctx.lower_contains_any = lower_contains_any
|
||||||
|
ctx.scratchpad_window_matches = scratchpad_window_matches
|
||||||
|
ctx.is_scratchpad_window = is_scratchpad_window
|
||||||
|
ctx.matching_scratchpad_name = matching_scratchpad_name
|
||||||
|
ctx.scratchpad_workspace = scratchpad_workspace
|
||||||
|
ctx.as_number = as_number
|
||||||
|
ctx.logical_monitor_dimension = logical_monitor_dimension
|
||||||
|
ctx.split_tsv = split_tsv
|
||||||
|
ctx.monitor_from_reserved_fields = monitor_from_reserved_fields
|
||||||
|
ctx.monitor_from_reserved_lines = monitor_from_reserved_lines
|
||||||
|
ctx.monitor_from_reserved_cache = monitor_from_reserved_cache
|
||||||
|
ctx.refresh_monitor_reserved_cache = refresh_monitor_reserved_cache
|
||||||
|
ctx.monitor_workarea = monitor_workarea
|
||||||
|
ctx.matching_scratchpad_windows = matching_scratchpad_windows
|
||||||
|
ctx.apply_scratchpad_geometry = apply_scratchpad_geometry
|
||||||
|
ctx.schedule_scratchpad_geometry = schedule_scratchpad_geometry
|
||||||
|
ctx.hide_scratchpad_window = hide_scratchpad_window
|
||||||
|
ctx.show_scratchpad_window = show_scratchpad_window
|
||||||
|
ctx.scratchpad_is_visible = scratchpad_is_visible
|
||||||
|
ctx.active_scratchpad_windows = active_scratchpad_windows
|
||||||
|
ctx.hide_active_scratchpads = hide_active_scratchpads
|
||||||
|
ctx.refresh_active_scratchpad_geometries = refresh_active_scratchpad_geometries
|
||||||
|
ctx.refresh_active_scratchpad_geometries_later = refresh_active_scratchpad_geometries_later
|
||||||
|
ctx.refresh_shell_workarea_and_scratchpads = refresh_shell_workarea_and_scratchpads
|
||||||
|
ctx.adopt_matching_scratchpad_window = adopt_matching_scratchpad_window
|
||||||
|
ctx.toggle_scratchpad = toggle_scratchpad
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -30,8 +30,6 @@ return {
|
|||||||
},
|
},
|
||||||
|
|
||||||
max_workspace = 9,
|
max_workspace = 9,
|
||||||
scratchpad_size_ratio = 0.95,
|
|
||||||
dropdown_height_ratio = 0.5,
|
|
||||||
columns_layout = columns_layout,
|
columns_layout = columns_layout,
|
||||||
large_main_layout = large_main_layout,
|
large_main_layout = large_main_layout,
|
||||||
grid_layout = grid_layout,
|
grid_layout = grid_layout,
|
||||||
@@ -58,48 +56,4 @@ return {
|
|||||||
window_picker_candidates = {},
|
window_picker_candidates = {},
|
||||||
stack_update_timer = nil,
|
stack_update_timer = nil,
|
||||||
monocle_notice = nil,
|
monocle_notice = nil,
|
||||||
scratchpad_pending = {},
|
|
||||||
monitor_reserved_cache_path = (os.getenv("XDG_RUNTIME_DIR") or "/tmp") .. "/hyprland-monitor-reserved.tsv",
|
|
||||||
scratchpad_fallback_reserved_top = 60,
|
|
||||||
|
|
||||||
scratchpads = {
|
|
||||||
codex = {
|
|
||||||
command = "codex-desktop",
|
|
||||||
class = "codex-desktop",
|
|
||||||
},
|
|
||||||
htop = {
|
|
||||||
command = "alacritty --class htop-scratch --title htop -e htop",
|
|
||||||
class = "htop-scratch",
|
|
||||||
},
|
|
||||||
volume = {
|
|
||||||
command = "pavucontrol",
|
|
||||||
class = "org.pulseaudio.pavucontrol",
|
|
||||||
},
|
|
||||||
spotify = {
|
|
||||||
command = "spotify",
|
|
||||||
class = "spotify",
|
|
||||||
},
|
|
||||||
element = {
|
|
||||||
command = "element-desktop",
|
|
||||||
classes = { "Element", "electron" },
|
|
||||||
title = "Element",
|
|
||||||
},
|
|
||||||
slack = {
|
|
||||||
command = "slack",
|
|
||||||
class = "Slack",
|
|
||||||
},
|
|
||||||
messages = {
|
|
||||||
command = "google-chrome-stable --profile-directory=Default --app=https://messages.google.com/web/conversations",
|
|
||||||
class = "chrome-messages.google.com",
|
|
||||||
},
|
|
||||||
transmission = {
|
|
||||||
command = "transmission-gtk",
|
|
||||||
class = "transmission-gtk",
|
|
||||||
},
|
|
||||||
dropdown = {
|
|
||||||
command = "ghostty --config-file=/home/imalison/.config/ghostty/dropdown",
|
|
||||||
class = "com.mitchellh.ghostty.dropdown",
|
|
||||||
dropdown = true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,184 +102,6 @@ function M.setup(ctx)
|
|||||||
minimized_windows = hydrated
|
minimized_windows = hydrated
|
||||||
end
|
end
|
||||||
|
|
||||||
local function window_workspace_name(window)
|
|
||||||
return window and window.workspace and window.workspace.name or ""
|
|
||||||
end
|
|
||||||
|
|
||||||
local function scratchpad_workspace(name)
|
|
||||||
return "special:scratch-" .. name
|
|
||||||
end
|
|
||||||
|
|
||||||
local function as_number(value, default)
|
|
||||||
local number = tonumber(value)
|
|
||||||
if number == nil then
|
|
||||||
return default
|
|
||||||
end
|
|
||||||
return number
|
|
||||||
end
|
|
||||||
|
|
||||||
local function logical_monitor_dimension(value, scale)
|
|
||||||
value = as_number(value, 0)
|
|
||||||
scale = as_number(scale, 1)
|
|
||||||
if scale <= 0 then
|
|
||||||
scale = 1
|
|
||||||
end
|
|
||||||
return math.floor((value / scale) + 0.5)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function split_tsv(line)
|
|
||||||
local fields = {}
|
|
||||||
for field in (line .. "\t"):gmatch("([^\t]*)\t") do
|
|
||||||
fields[#fields + 1] = field
|
|
||||||
end
|
|
||||||
return fields
|
|
||||||
end
|
|
||||||
|
|
||||||
local function monitor_from_reserved_fields(monitor, fields)
|
|
||||||
if not monitor or not monitor.name or fields[1] ~= monitor.name or #fields < 10 then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
name = monitor.name,
|
|
||||||
x = tonumber(fields[2]),
|
|
||||||
y = tonumber(fields[3]),
|
|
||||||
width = tonumber(fields[4]),
|
|
||||||
height = tonumber(fields[5]),
|
|
||||||
scale = tonumber(fields[6]),
|
|
||||||
reserved = {
|
|
||||||
tonumber(fields[7]),
|
|
||||||
tonumber(fields[8]),
|
|
||||||
tonumber(fields[9]),
|
|
||||||
tonumber(fields[10]),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function monitor_from_reserved_lines(monitor, lines)
|
|
||||||
if not monitor or not monitor.name then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
for line in lines do
|
|
||||||
local cached = monitor_from_reserved_fields(monitor, split_tsv(line))
|
|
||||||
if cached then
|
|
||||||
return cached
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function monitor_from_reserved_cache(monitor)
|
|
||||||
if verify_config or not monitor or not monitor.name then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local file = io.open(monitor_reserved_cache_path, "r")
|
|
||||||
if not file then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local cached = monitor_from_reserved_lines(monitor, file:lines())
|
|
||||||
file:close()
|
|
||||||
return cached
|
|
||||||
end
|
|
||||||
|
|
||||||
local function refresh_monitor_reserved_cache(delay)
|
|
||||||
if verify_config then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local command = string.format(
|
|
||||||
[=[sleep %.2f; cache="${XDG_RUNTIME_DIR:-/tmp}/hyprland-monitor-reserved.tsv"; tmp="$cache.tmp"; /run/current-system/sw/bin/hyprctl -j monitors 2>/dev/null | /run/current-system/sw/bin/jq -r '.[] | [.name, .x, .y, .width, .height, .scale, .reserved[0], .reserved[1], .reserved[2], .reserved[3]] | @tsv' > "$tmp" && mv "$tmp" "$cache"]=],
|
|
||||||
as_number(delay, 0)
|
|
||||||
)
|
|
||||||
hl.exec_cmd("sh -lc " .. shell_quote(command))
|
|
||||||
end
|
|
||||||
|
|
||||||
local function monitor_workarea(monitor)
|
|
||||||
monitor = monitor_from_reserved_cache(monitor) or monitor
|
|
||||||
local width = logical_monitor_dimension(monitor.width, monitor.scale)
|
|
||||||
local height = logical_monitor_dimension(monitor.height, monitor.scale)
|
|
||||||
local reserved = monitor.reserved or { 0, scratchpad_fallback_reserved_top, 0, 0 }
|
|
||||||
local left = math.floor(as_number(reserved[1], 0))
|
|
||||||
local top = math.floor(as_number(reserved[2], 0))
|
|
||||||
local right = math.floor(as_number(reserved[3], 0))
|
|
||||||
local bottom = math.floor(as_number(reserved[4], 0))
|
|
||||||
local work_width = width - left - right
|
|
||||||
local work_height = height - top - bottom
|
|
||||||
|
|
||||||
if work_width <= 0 then
|
|
||||||
left = 0
|
|
||||||
right = 0
|
|
||||||
work_width = width
|
|
||||||
end
|
|
||||||
if work_height <= 0 then
|
|
||||||
top = 0
|
|
||||||
bottom = 0
|
|
||||||
work_height = height
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
x = math.floor(as_number(monitor.x, 0)) + left,
|
|
||||||
y = math.floor(as_number(monitor.y, 0)) + top,
|
|
||||||
width = work_width,
|
|
||||||
height = work_height,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local function matching_scratchpad_windows(name)
|
|
||||||
local def = scratchpads[name]
|
|
||||||
local windows = {}
|
|
||||||
if not def then
|
|
||||||
return windows
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, window in ipairs(hl.get_windows()) do
|
|
||||||
if scratchpad_window_matches(window, def) then
|
|
||||||
windows[#windows + 1] = window
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return windows
|
|
||||||
end
|
|
||||||
|
|
||||||
local function apply_scratchpad_geometry(name, window, target_monitor)
|
|
||||||
local def = scratchpads[name]
|
|
||||||
local monitor = target_monitor or hl.get_active_monitor()
|
|
||||||
if not def or not window or not monitor then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local workarea = monitor_workarea(monitor)
|
|
||||||
local width
|
|
||||||
local height
|
|
||||||
local x
|
|
||||||
local y
|
|
||||||
if def.dropdown then
|
|
||||||
width = workarea.width
|
|
||||||
height = math.floor(workarea.height * dropdown_height_ratio)
|
|
||||||
x = workarea.x
|
|
||||||
y = workarea.y
|
|
||||||
else
|
|
||||||
width = math.floor(workarea.width * scratchpad_size_ratio)
|
|
||||||
height = math.floor(workarea.height * scratchpad_size_ratio)
|
|
||||||
x = workarea.x + math.floor((workarea.width - width) / 2)
|
|
||||||
y = workarea.y + math.floor((workarea.height - height) / 2)
|
|
||||||
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 }))
|
|
||||||
dispatch(hl.dsp.window.resize({ x = width, y = height, relative = false, window = selector }))
|
|
||||||
dispatch(hl.dsp.window.move({ x = x, y = y, relative = false, window = selector }))
|
|
||||||
if def.dropdown then
|
|
||||||
dispatch(hl.dsp.window.set_prop({ prop = "border_size", value = "0", window = selector }))
|
|
||||||
dispatch(hl.dsp.window.set_prop({ prop = "no_shadow", value = "1", window = selector }))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function float_active_window_preserving_tiled_geometry()
|
local function float_active_window_preserving_tiled_geometry()
|
||||||
local geometry = tiled_window_geometry(hl.get_active_window())
|
local geometry = tiled_window_geometry(hl.get_active_window())
|
||||||
dispatch(hl.dsp.window.float({ action = "enable", window = geometry and geometry.selector or nil }))
|
dispatch(hl.dsp.window.float({ action = "enable", window = geometry and geometry.selector or nil }))
|
||||||
@@ -319,90 +141,6 @@ function M.setup(ctx)
|
|||||||
dispatch(hl.dsp.window.pin({ action = "enable", window = selector }))
|
dispatch(hl.dsp.window.pin({ action = "enable", window = selector }))
|
||||||
end
|
end
|
||||||
|
|
||||||
local function schedule_scratchpad_geometry(name, window, target_monitor)
|
|
||||||
hl.timer(function()
|
|
||||||
apply_scratchpad_geometry(name, window, target_monitor)
|
|
||||||
end, { timeout = 50, type = "oneshot" })
|
|
||||||
end
|
|
||||||
|
|
||||||
local function hide_scratchpad_window(name, window)
|
|
||||||
remove_minimized_window(window)
|
|
||||||
move_window_to_workspace(scratchpad_workspace(name), false, window)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function show_scratchpad_window(name, window, workspace, target_monitor)
|
|
||||||
workspace = workspace or active_workspace()
|
|
||||||
if not workspace then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
remove_minimized_window(window)
|
|
||||||
move_window_to_workspace(workspace.id, false, window)
|
|
||||||
dispatch(hl.dsp.focus({ window = window_selector(window) }))
|
|
||||||
schedule_scratchpad_geometry(name, window, target_monitor or hl.get_active_monitor())
|
|
||||||
end
|
|
||||||
|
|
||||||
local function scratchpad_is_visible(window)
|
|
||||||
local workspace = active_workspace()
|
|
||||||
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.
|
|
||||||
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 and scratchpad_is_visible(window) then
|
|
||||||
windows[#windows + 1] = {
|
|
||||||
name = name,
|
|
||||||
window = window,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return windows
|
|
||||||
end
|
|
||||||
|
|
||||||
local function hide_active_scratchpads(except_name)
|
|
||||||
for _, active in ipairs(active_scratchpad_windows(except_name)) do
|
|
||||||
hide_scratchpad_window(active.name, active.window)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function refresh_active_scratchpad_geometries()
|
|
||||||
local monitor = hl.get_active_monitor()
|
|
||||||
for _, active in ipairs(active_scratchpad_windows()) do
|
|
||||||
schedule_scratchpad_geometry(active.name, active.window, monitor)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function refresh_active_scratchpad_geometries_later(timeout)
|
|
||||||
hl.timer(refresh_active_scratchpad_geometries, { timeout = timeout or 300, type = "oneshot" })
|
|
||||||
end
|
|
||||||
|
|
||||||
local function refresh_shell_workarea_and_scratchpads()
|
|
||||||
refresh_monitor_reserved_cache(0.15)
|
|
||||||
refresh_active_scratchpad_geometries_later(400)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function adopt_matching_scratchpad_window(window)
|
|
||||||
if not window then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
for name, def in pairs(scratchpads) do
|
|
||||||
if scratchpad_window_matches(window, def) then
|
|
||||||
if scratchpad_pending[name] then
|
|
||||||
local pending = scratchpad_pending[name]
|
|
||||||
scratchpad_pending[name] = nil
|
|
||||||
show_scratchpad_window(name, window, pending.workspace or active_workspace(), pending.monitor or hl.get_active_monitor())
|
|
||||||
elseif scratchpad_is_visible(window) then
|
|
||||||
schedule_scratchpad_geometry(name, window, hl.get_active_monitor())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function current_minimized_windows()
|
local function current_minimized_windows()
|
||||||
hydrate_minimized_windows()
|
hydrate_minimized_windows()
|
||||||
|
|
||||||
@@ -705,49 +443,6 @@ function M.setup(ctx)
|
|||||||
minimized_windows = remaining
|
minimized_windows = remaining
|
||||||
end
|
end
|
||||||
|
|
||||||
local function toggle_scratchpad(name)
|
|
||||||
local def = scratchpads[name]
|
|
||||||
if not def then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if current_layout == monocle_layout then
|
|
||||||
set_layout(columns_layout)
|
|
||||||
end
|
|
||||||
|
|
||||||
local windows = matching_scratchpad_windows(name)
|
|
||||||
if #windows == 0 then
|
|
||||||
hide_active_scratchpads(name)
|
|
||||||
scratchpad_pending[name] = {
|
|
||||||
monitor = hl.get_active_monitor(),
|
|
||||||
workspace = active_workspace(),
|
|
||||||
}
|
|
||||||
hl.exec_cmd(def.command)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local any_visible = false
|
|
||||||
for _, window in ipairs(windows) do
|
|
||||||
if scratchpad_is_visible(window) then
|
|
||||||
any_visible = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if any_visible then
|
|
||||||
for _, window in ipairs(windows) do
|
|
||||||
hide_scratchpad_window(name, window)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
hide_active_scratchpads(name)
|
|
||||||
local workspace = active_workspace()
|
|
||||||
local target_monitor = hl.get_active_monitor()
|
|
||||||
for _, window in ipairs(windows) do
|
|
||||||
show_scratchpad_window(name, window, workspace, target_monitor)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ctx.same_class_windows = same_class_windows
|
ctx.same_class_windows = same_class_windows
|
||||||
ctx.short_text = short_text
|
ctx.short_text = short_text
|
||||||
ctx.normal_windows = normal_windows
|
ctx.normal_windows = normal_windows
|
||||||
@@ -755,32 +450,10 @@ function M.setup(ctx)
|
|||||||
ctx.remove_minimized_window = remove_minimized_window
|
ctx.remove_minimized_window = remove_minimized_window
|
||||||
ctx.add_minimized_window = add_minimized_window
|
ctx.add_minimized_window = add_minimized_window
|
||||||
ctx.hydrate_minimized_windows = hydrate_minimized_windows
|
ctx.hydrate_minimized_windows = hydrate_minimized_windows
|
||||||
ctx.window_workspace_name = window_workspace_name
|
|
||||||
ctx.scratchpad_workspace = scratchpad_workspace
|
|
||||||
ctx.as_number = as_number
|
|
||||||
ctx.logical_monitor_dimension = logical_monitor_dimension
|
|
||||||
ctx.split_tsv = split_tsv
|
|
||||||
ctx.monitor_from_reserved_fields = monitor_from_reserved_fields
|
|
||||||
ctx.monitor_from_reserved_lines = monitor_from_reserved_lines
|
|
||||||
ctx.monitor_from_reserved_cache = monitor_from_reserved_cache
|
|
||||||
ctx.refresh_monitor_reserved_cache = refresh_monitor_reserved_cache
|
|
||||||
ctx.monitor_workarea = monitor_workarea
|
|
||||||
ctx.matching_scratchpad_windows = matching_scratchpad_windows
|
|
||||||
ctx.apply_scratchpad_geometry = apply_scratchpad_geometry
|
|
||||||
ctx.float_active_window_preserving_tiled_geometry = float_active_window_preserving_tiled_geometry
|
ctx.float_active_window_preserving_tiled_geometry = float_active_window_preserving_tiled_geometry
|
||||||
ctx.float_and_drag_active_window = float_and_drag_active_window
|
ctx.float_and_drag_active_window = float_and_drag_active_window
|
||||||
ctx.float_and_resize_active_window = float_and_resize_active_window
|
ctx.float_and_resize_active_window = float_and_resize_active_window
|
||||||
ctx.toggle_pinned_active_window = toggle_pinned_active_window
|
ctx.toggle_pinned_active_window = toggle_pinned_active_window
|
||||||
ctx.schedule_scratchpad_geometry = schedule_scratchpad_geometry
|
|
||||||
ctx.hide_scratchpad_window = hide_scratchpad_window
|
|
||||||
ctx.show_scratchpad_window = show_scratchpad_window
|
|
||||||
ctx.scratchpad_is_visible = scratchpad_is_visible
|
|
||||||
ctx.active_scratchpad_windows = active_scratchpad_windows
|
|
||||||
ctx.hide_active_scratchpads = hide_active_scratchpads
|
|
||||||
ctx.refresh_active_scratchpad_geometries = refresh_active_scratchpad_geometries
|
|
||||||
ctx.refresh_active_scratchpad_geometries_later = refresh_active_scratchpad_geometries_later
|
|
||||||
ctx.refresh_shell_workarea_and_scratchpads = refresh_shell_workarea_and_scratchpads
|
|
||||||
ctx.adopt_matching_scratchpad_window = adopt_matching_scratchpad_window
|
|
||||||
ctx.current_minimized_windows = current_minimized_windows
|
ctx.current_minimized_windows = current_minimized_windows
|
||||||
ctx.restore_minimized_window = restore_minimized_window
|
ctx.restore_minimized_window = restore_minimized_window
|
||||||
ctx.window_picker_candidates_for = window_picker_candidates_for
|
ctx.window_picker_candidates_for = window_picker_candidates_for
|
||||||
@@ -795,7 +468,6 @@ function M.setup(ctx)
|
|||||||
ctx.restore_all_minimized = restore_all_minimized
|
ctx.restore_all_minimized = restore_all_minimized
|
||||||
ctx.minimize_other_classes = minimize_other_classes
|
ctx.minimize_other_classes = minimize_other_classes
|
||||||
ctx.restore_focused_class = restore_focused_class
|
ctx.restore_focused_class = restore_focused_class
|
||||||
ctx.toggle_scratchpad = toggle_scratchpad
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
Reference in New Issue
Block a user