Split Hyprland Lua config into modules

This commit is contained in:
2026-05-09 12:46:13 -07:00
parent c12b9c05db
commit fdaaf130f2
12 changed files with 2649 additions and 2426 deletions

View File

@@ -137,8 +137,9 @@ notifications, SNI/tray support, fonts, and app defaults.
The currently important pieces are: The currently important pieces are:
- Hyprland configuration in [[file:dotfiles/config/hypr/hyprland.lua][dotfiles/config/hypr/hyprland.lua]], backed by custom - Hyprland configuration in [[file:dotfiles/config/hypr/hyprland.lua][dotfiles/config/hypr/hyprland.lua]], with imported Lua
plugin inputs in the NixOS flake. modules under [[file:dotfiles/config/hypr/hyprland/][dotfiles/config/hypr/hyprland/]], backed by custom plugin inputs in
the NixOS flake.
- XMonad configuration in [[file:dotfiles/config/xmonad/xmonad.hs][dotfiles/config/xmonad/xmonad.hs]], with upstream - XMonad configuration in [[file:dotfiles/config/xmonad/xmonad.hs][dotfiles/config/xmonad/xmonad.hs]], with upstream
=xmonad= and =xmonad-contrib= available as submodules/checkouts. =xmonad= and =xmonad-contrib= available as submodules/checkouts.
- Taffybar configuration in [[file:dotfiles/config/taffybar/taffybar.hs][dotfiles/config/taffybar/taffybar.hs]], plus a local - Taffybar configuration in [[file:dotfiles/config/taffybar/taffybar.hs][dotfiles/config/taffybar/taffybar.hs]], plus a local

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,269 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
bind(main_mod .. " + P", exec(launcher_command))
bind(main_mod .. " + SHIFT + P", exec(run_menu))
bind(hyper .. " + SHIFT + N", exec(shell_ui_command .. " control-center"))
bind(hyper .. " + CTRL + N", exec(shell_ui_command .. " settings"))
bind(main_mod .. " + SHIFT + Return", exec(terminal))
bind(main_mod .. " + Q", exec("hyprctl reload"))
bind(main_mod .. " + SHIFT + C", hl.dsp.window.close())
bind(main_mod .. " + SHIFT + Q", hl.dsp.exit())
bind(main_mod .. " + E", exec("emacsclient --eval '(emacs-everywhere)'"))
bind(main_mod .. " + V", exec("wl-paste | xdotool type --file -"))
bind(main_mod .. " + Tab", hyprwinview({
action = "show",
start_in_filter_mode = true,
default_action = "bring",
}), overview_bind_opts)
bind(main_mod .. " + SHIFT + Tab", hyprwinview({
action = "show",
include_current_workspace = false,
start_in_filter_mode = true,
default_action = "bring",
}), overview_bind_opts)
bind(main_mod .. " + SHIFT + slash", hyprwinview({ action = "toggle-filter" }), overview_bind_opts)
bind("ALT + Tab", hyprexpo("open"), overview_bind_opts)
bind("ALT + SHIFT + Tab", hyprexpo("bring"), overview_bind_opts)
bind(main_mod .. " + G", hyprwinview({
action = "show",
start_in_filter_mode = true,
default_action = "select",
}), overview_bind_opts)
bind(main_mod .. " + B", hyprwinview({
action = "show",
start_in_filter_mode = true,
default_action = "bring",
}), overview_bind_opts)
bind(main_mod .. " + SHIFT + B", hyprwinview({
action = "show",
start_in_filter_mode = true,
default_action = "bring-replace",
}), overview_bind_opts)
bind(main_mod .. " + W", function()
focus_direction("up")
end)
bind(main_mod .. " + S", function()
focus_direction("down")
end)
bind(main_mod .. " + A", function()
focus_direction("left")
end)
bind(main_mod .. " + D", function()
focus_direction("right")
end)
bind(main_mod .. " + SHIFT + W", function()
swap_direction("up")
end)
bind(main_mod .. " + SHIFT + S", function()
swap_direction("down")
end)
bind(main_mod .. " + SHIFT + A", function()
swap_direction("left")
end)
bind(main_mod .. " + SHIFT + D", function()
swap_direction("right")
end)
bind(main_mod .. " + CTRL + W", function()
move_window_to_monitor("u", false)
end)
bind(main_mod .. " + CTRL + S", function()
move_window_to_monitor("d", false)
end)
bind(main_mod .. " + CTRL + A", function()
move_window_to_monitor("l", false)
end)
bind(main_mod .. " + CTRL + D", function()
move_window_to_monitor("r", false)
end)
bind(main_mod .. " + CTRL + SHIFT + W", function()
move_window_to_empty_workspace_on_monitor("u")
end)
bind(main_mod .. " + CTRL + SHIFT + S", function()
move_window_to_empty_workspace_on_monitor("d")
end)
bind(main_mod .. " + CTRL + SHIFT + A", function()
move_window_to_empty_workspace_on_monitor("l")
end)
bind(main_mod .. " + CTRL + SHIFT + D", function()
move_window_to_empty_workspace_on_monitor("r")
end)
hl.define_submap("swap-workspace", function()
for i = 1, 9 do
local workspace_id = i
bind(tostring(i), function()
swap_current_workspace_with(workspace_id)
dispatch(hl.dsp.submap("reset"))
end)
end
bind("Escape", hl.dsp.submap("reset"))
bind("catchall", hl.dsp.submap("reset"))
end)
hl.define_submap("window-picker", function()
for i = 1, 9 do
local index = i
bind(tostring(i), function()
activate_window_picker_candidate(index)
end)
end
bind("Escape", hl.dsp.submap("reset"))
bind("catchall", hl.dsp.submap("reset"))
end)
bind(mod_alt .. " + SHIFT + W", hl.dsp.window.resize({ x = 0, y = -50, relative = true }), { repeating = true })
bind(mod_alt .. " + SHIFT + S", hl.dsp.window.resize({ x = 0, y = 50, relative = true }), { repeating = true })
bind(mod_alt .. " + SHIFT + A", hl.dsp.window.resize({ x = -50, y = 0, relative = true }), { repeating = true })
bind(mod_alt .. " + SHIFT + D", hl.dsp.window.resize({ x = 50, y = 0, relative = true }), { repeating = true })
bind(hyper .. " + W", hl.dsp.focus({ monitor = "u" }))
bind(hyper .. " + S", hl.dsp.focus({ monitor = "d" }))
bind(hyper .. " + A", hl.dsp.focus({ monitor = "l" }))
bind(hyper .. " + D", hl.dsp.focus({ monitor = "r" }))
bind(hyper .. " + SHIFT + W", function()
move_window_to_monitor("u", true)
end)
bind(hyper .. " + SHIFT + S", function()
move_window_to_monitor("d", true)
end)
bind(hyper .. " + SHIFT + A", function()
move_window_to_monitor("l", true)
end)
bind(hyper .. " + SHIFT + D", function()
move_window_to_monitor("r", true)
end)
bind(main_mod .. " + Space", cycle_layout_or_restore_tabbed_group)
bind(main_mod .. " + SHIFT + Space", force_columns_layout)
bind(main_mod .. " + CTRL + Space", gather_workspace_into_tabbed_group)
bind(main_mod .. " + bracketright", monocle_next)
bind(main_mod .. " + bracketleft", monocle_prev)
bind(main_mod .. " + T", hl.dsp.window.float({ action = "disable" }))
bind(main_mod .. " + O", toggle_pinned_active_window)
bind(main_mod .. " + M", minimize_active_window)
bind(main_mod .. " + SHIFT + M", restore_last_minimized)
bind(main_mod .. " + CTRL + SHIFT + M", function()
enter_window_picker("minimized")
end)
bind(main_mod .. " + SHIFT + equal", schedule_nstack_count_update)
bind(main_mod .. " + CTRL + M", hl.dsp.window.toggle_swallow())
bind(main_mod .. " + SHIFT + E", function()
move_to_next_empty_workspace(true)
end)
bind(main_mod .. " + CTRL + E", function()
move_to_next_empty_workspace(false)
end)
bind(main_mod .. " + apostrophe", focus_next_class)
bind(mod_alt .. " + W", show_active_window_info)
bind(main_mod .. " + X", exec("rofi_command.sh"))
bind(main_mod .. " + SHIFT + X", hl.dsp.workspace.toggle_special("NSP"))
bind(mod_alt .. " + C", function()
toggle_scratchpad("codex")
end)
bind(mod_alt .. " + E", function()
toggle_scratchpad("element")
end)
bind(mod_alt .. " + H", function()
toggle_scratchpad("htop")
end)
bind(mod_alt .. " + K", function()
toggle_scratchpad("slack")
end)
bind(mod_alt .. " + M", function()
toggle_scratchpad("messages")
end)
bind(mod_alt .. " + S", function()
toggle_scratchpad("spotify")
end)
bind(mod_alt .. " + T", function()
toggle_scratchpad("transmission")
end)
bind(mod_alt .. " + V", function()
toggle_scratchpad("volume")
end)
bind(mod_alt .. " + grave", function()
toggle_scratchpad("dropdown")
end)
bind(mod_alt .. " + Space", minimize_other_classes)
bind(mod_alt .. " + SHIFT + Space", restore_focused_class)
bind(mod_alt .. " + Return", restore_all_minimized)
for i = 1, 9 do
local workspace = tostring(i)
bind(main_mod .. " + " .. workspace, hl.dsp.focus({ workspace = workspace, on_current_monitor = true }))
bind(main_mod .. " + SHIFT + " .. workspace, hl.dsp.window.move({ workspace = workspace, follow = false }))
bind(main_mod .. " + CTRL + " .. workspace, function()
dispatch(hl.dsp.window.move({ workspace = workspace, follow = false }))
dispatch(hl.dsp.focus({ workspace = workspace, on_current_monitor = true }))
end)
end
bind(main_mod .. " + backslash", workspacehistory("cycle", 1))
bind(main_mod .. " + slash", workspacehistory("cycle", -1))
bind(main_mod .. " + Escape", workspacehistory("cancel"))
bind(main_mod .. " + Z", hl.dsp.focus({ monitor = "+1" }))
bind(main_mod .. " + SHIFT + Z", hl.dsp.window.move({ monitor = "+1" }))
bind(main_mod .. " + mouse_down", function()
cycle_workspace(1)
end)
bind(main_mod .. " + mouse_up", function()
cycle_workspace(-1)
end)
bind(hyper .. " + E", focus_next_empty_workspace)
bind(hyper .. " + 5", enter_workspace_swap_mode)
bind(hyper .. " + G", gather_focused_class)
bind(main_mod .. " + I", exec("set_volume --unmute --change-volume +5"), { repeating = true })
bind(main_mod .. " + K", exec("set_volume --unmute --change-volume -5"), { repeating = true })
bind(main_mod .. " + U", exec("set_volume --toggle-mute"))
bind(main_mod .. " + semicolon", exec("playerctl play-pause"))
bind(main_mod .. " + L", exec("playerctl next"))
bind(main_mod .. " + J", exec("playerctl previous"))
bind("XF86AudioPlay", exec("playerctl play-pause"))
bind("XF86AudioPause", exec("playerctl play-pause"))
bind("XF86AudioNext", exec("playerctl next"))
bind("XF86AudioPrev", exec("playerctl previous"))
bind("XF86AudioRaiseVolume", exec("set_volume --unmute --change-volume +5"), { repeating = true })
bind("XF86AudioLowerVolume", exec("set_volume --unmute --change-volume -5"), { repeating = true })
bind("XF86AudioMute", exec("set_volume --toggle-mute"))
bind("XF86MonBrightnessUp", exec("brightness.sh up"), { repeating = true })
bind("XF86MonBrightnessDown", exec("brightness.sh down"), { repeating = true })
bind(hyper .. " + V", exec([[cliphist list | rofi -dmenu -p "Clipboard" | cliphist decode | wl-copy]]))
bind(hyper .. " + P", exec("rofi-pass"))
bind(hyper .. " + H", exec([[grim -g "$(slurp)" - | swappy -f -]]))
bind(hyper .. " + C", exec("rofi_tmcodex.sh"))
bind(hyper .. " + SHIFT + C", exec("rofi_tmcodex.sh resume"))
bind(hyper .. " + SHIFT + L", exec("hyprlock"))
bind(hyper .. " + L", exec("hypr_rofi_layout"))
bind(hyper .. " + K", exec("rofi_kill_process.sh"))
bind(hyper .. " + SHIFT + K", exec("rofi_kill_all.sh"))
bind(hyper .. " + R", exec("rofi-systemd"))
bind(hyper .. " + slash", function()
hl.exec_cmd("toggle_taffybar")
refresh_monitor_reserved_cache(0.25)
refresh_active_scratchpad_geometries_later(600)
end)
bind(hyper .. " + I", exec("rofi_select_input.hs"))
bind(hyper .. " + backslash", exec("/home/imalison/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle"))
bind(hyper .. " + SHIFT + backslash", workspacehistory("debug"))
bind(hyper .. " + O", exec("rofi_paswitch"))
bind(hyper .. " + comma", exec("rofi_wallpaper.sh"))
bind(hyper .. " + SHIFT + comma", exec("/home/imalison/dotfiles/dotfiles/lib/bin/neowall-wallpaper toggle"))
bind(hyper .. " + Y", exec("rofi_agentic_skill"))
bind(main_mod .. " + R", exec("hyprctl reload"))
bind(main_mod .. " + mouse:272", float_and_drag_active_window)
bind(main_mod .. " + mouse:273", float_and_resize_active_window)
end
return M

View File

@@ -0,0 +1,584 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function command_line_contains(needle)
local command_line = io.open("/proc/self/cmdline", "rb")
if not command_line then
return false
end
local contents = command_line:read("*a") or ""
command_line:close()
return contents:find(needle, 1, true) ~= nil
end
verify_config = command_line_contains("--verify-config")
local function bind(keys, dispatcher, opts)
hl.bind(keys, dispatcher, opts)
end
local function exec(command)
return hl.dsp.exec_cmd(command)
end
local function dispatch(dispatcher)
return hl.dispatch(dispatcher)
end
local function shell_quote(value)
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
end
local function overview_trace(label)
local enabled = io.open(overview_trace_enabled_path, "r")
if not enabled then
return
end
enabled:close()
local trace = io.open(overview_trace_path, "a")
if trace then
trace:write(os.date("%Y-%m-%d %H:%M:%S "), label, "\n")
trace:close()
end
end
local function window_selector(window)
if not window or not window.address then
return nil
end
return "address:" .. tostring(window.address)
end
local function hyprexpo(action)
action = action or "toggle"
return function()
overview_trace("hyprexpo " .. tostring(action))
if hl.plugin and hl.plugin.hyprexpo and hl.plugin.hyprexpo.expo then
hl.plugin.hyprexpo.expo(action)
else
hl.notification.create({
text = "hyprexpo is not loaded",
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
end
end
end
local function hyprwinview(action)
return function()
local label = "hyprwinview"
if type(action) == "table" and action.action then
label = label .. " " .. tostring(action.action)
elseif type(action) ~= "table" and action ~= nil then
label = label .. " " .. tostring(action)
end
local function invoke()
overview_trace(label)
if hl.plugin and hl.plugin.hyprwinview and hl.plugin.hyprwinview.overview then
hl.plugin.hyprwinview.overview(action)
else
hl.notification.create({
text = "hyprwinview is not loaded",
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
end
end
invoke()
end
end
local function workspacehistory(action, arg)
return function()
if hl.plugin and hl.plugin.workspacehistory and hl.plugin.workspacehistory[action] then
hl.plugin.workspacehistory[action](arg)
else
hl.notification.create({
text = "workspacehistory is not loaded",
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
end
end
end
local function apply_nstack_config()
if verify_config or not enable_nstack or not configure_nstack_plugin_from_lua then
return
end
hl.config({
plugin = {
nstack = {
layout = {
orientation = "left",
new_on_top = false,
new_near_focused = true,
new_is_master = false,
no_gaps_when_only = true,
special_scale_factor = 0.8,
inherit_fullscreen = true,
stacks = 1,
center_single_master = false,
mfact = 0.0,
single_mfact = 1.0,
},
},
},
})
end
local function apply_hyprexpo_config()
if verify_config or not enable_hyprexpo then
return
end
hl.config({
plugin = {
hyprexpo = {
columns = 3,
gap_size = 5,
bg_col = "rgba(111111ff)",
workspace_method = "center current",
skip_empty = false,
max_workspace = max_workspace,
show_workspace_numbers = true,
workspace_number_color = "rgba(edb443ff)",
gesture_distance = 200,
},
},
})
end
local function apply_hyprwinview_config()
if verify_config or not enable_hyprwinview then
return
end
hl.config({
plugin = {
hyprwinview = {
gap_size = 24,
margin = 48,
background = "rgba(10101400)",
background_blur = 1,
border_col = "rgba(ffffff33)",
hover_border_col = "rgba(66ccffee)",
border_size = 3,
window_order = "application",
keys_default_action = "return,enter,space,g,f",
keys_filter_toggle = "/",
show_app_icon = 1,
app_icon_size = 48,
app_icon_theme_source = "auto",
app_icon_position = "bottom right",
app_icon_margin_x = 12,
app_icon_margin_y = 12,
app_icon_margin_relative_x = 0.0,
app_icon_margin_relative_y = 0.0,
app_icon_offset_x = 0,
app_icon_offset_y = 0,
app_icon_backplate_col = "rgba(00000066)",
app_icon_backplate_padding = 6,
show_window_text = 1,
window_text_font = "Sans",
window_text_size = 14,
window_text_color = "rgba(ffffffff)",
window_text_backplate_col = "rgba(00000099)",
window_text_padding = 6,
filter_animation_ms = 140,
animation = "workspace_zoom",
animation_in_ms = 280,
animation_out_ms = 220,
animation_speed = 1.0,
animation_scale = 0.94,
animation_stagger_ms = 16,
animation_stagger_max_ms = 120,
},
},
})
if hl.plugin and hl.plugin.hyprwinview and hl.plugin.hyprwinview.configure then
hl.plugin.hyprwinview.configure({
keys = {
left = { "a", "h", "left" },
right = { "d", "l", "right" },
up = { "w", "k", "up" },
down = { "s", "j", "down" },
default_action = { "return", "enter", "space", "g", "f" },
bring = { "b", "shift+return", "shift+space" },
bring_replace = { "shift + b" },
close = { "escape", "q" },
filter_toggle = { "/" },
},
})
end
end
local function active_workspace()
return hl.get_active_workspace()
end
local function active_workspace_id()
local workspace = active_workspace()
if workspace and type(workspace.id) == "number" and workspace.id >= 1 then
return math.min(max_workspace, math.max(1, workspace.id))
end
return 1
end
local function workspace_key(workspace)
workspace = workspace or active_workspace()
if workspace and workspace.id then
return tostring(workspace.id)
end
return tostring(active_workspace_id())
end
local function current_workspace_layout()
return workspace_layouts[workspace_key()] or columns_layout
end
local function write_layout_state()
local runtime_dir = os.getenv("XDG_RUNTIME_DIR")
if not runtime_dir then
return
end
local file = io.open(runtime_dir .. "/hyprland-layout-state", "w")
if not file then
return
end
local workspace = active_workspace()
file:write("workspace=", workspace_key(workspace), "\n")
file:write("layout=", current_layout, "\n")
for key, layout in pairs(workspace_layouts) do
file:write("workspace.", tostring(key), "=", tostring(layout), "\n")
end
file:close()
end
local function is_normal_workspace(workspace)
return workspace and not workspace.special and workspace.id and workspace.id >= 1
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)
if not left or not right then
return false
end
if left.name and right.name and tostring(left.name) == tostring(right.name) then
return true
end
return left.id and right.id and left.id == right.id
end
local function is_minimized_workspace(workspace)
if not workspace then
return false
end
local name = tostring(workspace.name or "")
return name == minimized_workspace or name == "minimized" or (workspace.special and name:find("minimized", 1, true) ~= nil)
end
local function is_minimized_window(window)
return window and is_minimized_workspace(window.workspace)
end
local function is_normal_window(window)
return window
and window.mapped ~= false
and not window.hidden
and window.workspace
and is_normal_workspace(window.workspace)
and not is_scratchpad_window(window)
and not is_minimized_window(window)
end
local function tiled_windows(workspace)
local windows = {}
if not workspace then
return windows
end
for _, window in ipairs(hl.get_workspace_windows(workspace)) do
if not window.floating and not window.hidden then
windows[#windows + 1] = window
end
end
return windows
end
local function tiled_window_count(workspace)
return #tiled_windows(workspace)
end
local function sort_windows_by_focus_history(windows)
table.sort(windows, function(left, right)
return (left.focus_history_id or 0) < (right.focus_history_id or 0)
end)
end
local function window_address_set(windows)
local addresses = {}
for _, window in ipairs(windows) do
if window and window.address then
addresses[window.address] = true
end
end
return addresses
end
local function window_address_list(windows)
local addresses = {}
for _, window in ipairs(windows) do
if window and window.address then
addresses[#addresses + 1] = window.address
end
end
return addresses
end
local function window_address_in_set(window, addresses)
return window and window.address and addresses[window.address] or false
end
local function windows_by_address()
local windows = {}
for _, window in ipairs(hl.get_windows()) do
if window and window.address then
windows[window.address] = window
end
end
return windows
end
local function numeric_component(value, key, index)
if type(value) ~= "table" then
return 0
end
return tonumber(value[key] or value[index]) or 0
end
local function window_center(window)
local at = window and window.at or {}
local size = window and window.size or {}
return numeric_component(at, "x", 1) + numeric_component(size, "x", 1) / 2,
numeric_component(at, "y", 2) + numeric_component(size, "y", 2) / 2
end
local function tiled_window_geometry(window)
if not window or window.floating then
return nil
end
local selector = window_selector(window)
if not selector then
return nil
end
local at = window.at or {}
local size = window.size or {}
local width = math.floor(numeric_component(size, "x", 1))
local height = math.floor(numeric_component(size, "y", 2))
if width <= 0 or height <= 0 then
return nil
end
return {
selector = selector,
x = math.floor(numeric_component(at, "x", 1)),
y = math.floor(numeric_component(at, "y", 2)),
width = width,
height = height,
}
end
local function window_distance_squared(window, x, y)
local wx, wy = window_center(window)
local dx = wx - x
local dy = wy - y
return dx * dx + dy * dy
end
local function sort_windows_by_visual_position(windows)
table.sort(windows, function(left, right)
local left_x, left_y = window_center(left)
local right_x, right_y = window_center(right)
if math.abs(left_x - right_x) > 10 then
return left_x < right_x
end
if math.abs(left_y - right_y) > 10 then
return left_y < right_y
end
return tostring(left.address or "") < tostring(right.address or "")
end)
end
local function grouping_direction(window, anchor)
local wx, wy = window_center(window)
local ax, ay = window_center(anchor)
local dx = wx - ax
local dy = wy - ay
if math.abs(dx) >= math.abs(dy) then
return dx >= 0 and "left" or "right"
end
return dy >= 0 and "up" or "down"
end
local function grouping_directions(window, anchor)
local primary = grouping_direction(window, anchor)
local directions = { primary }
for _, direction in ipairs({ "left", "right", "up", "down" }) do
if direction ~= primary then
directions[#directions + 1] = direction
end
end
return directions
end
local function workspace_window_count(workspace_id)
local workspace = hl.get_workspace(tostring(workspace_id))
if not workspace then
return 0
end
return workspace.windows or tiled_window_count(workspace)
end
local function find_empty_workspace(target_monitor, exclude_id)
local unused_candidate = nil
local elsewhere_empty_candidate = nil
local target_monitor_name = target_monitor and target_monitor.name or nil
for i = 1, max_workspace do
if i ~= exclude_id then
local workspace = hl.get_workspace(tostring(i))
if not workspace then
unused_candidate = unused_candidate or i
elseif is_normal_workspace(workspace) and workspace_window_count(i) == 0 then
local monitor = workspace.monitor
if target_monitor_name and monitor and monitor.name == target_monitor_name then
return i
end
elsewhere_empty_candidate = elsewhere_empty_candidate or i
end
end
end
return unused_candidate or elsewhere_empty_candidate
end
ctx.command_line_contains = command_line_contains
ctx.bind = bind
ctx.exec = exec
ctx.dispatch = dispatch
ctx.shell_quote = shell_quote
ctx.overview_trace = overview_trace
ctx.window_selector = window_selector
ctx.hyprexpo = hyprexpo
ctx.hyprwinview = hyprwinview
ctx.workspacehistory = workspacehistory
ctx.apply_nstack_config = apply_nstack_config
ctx.apply_hyprexpo_config = apply_hyprexpo_config
ctx.apply_hyprwinview_config = apply_hyprwinview_config
ctx.active_workspace = active_workspace
ctx.active_workspace_id = active_workspace_id
ctx.workspace_key = workspace_key
ctx.current_workspace_layout = current_workspace_layout
ctx.write_layout_state = write_layout_state
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.is_minimized_workspace = is_minimized_workspace
ctx.is_minimized_window = is_minimized_window
ctx.is_normal_window = is_normal_window
ctx.tiled_windows = tiled_windows
ctx.tiled_window_count = tiled_window_count
ctx.sort_windows_by_focus_history = sort_windows_by_focus_history
ctx.window_address_set = window_address_set
ctx.window_address_list = window_address_list
ctx.window_address_in_set = window_address_in_set
ctx.windows_by_address = windows_by_address
ctx.numeric_component = numeric_component
ctx.window_center = window_center
ctx.tiled_window_geometry = tiled_window_geometry
ctx.window_distance_squared = window_distance_squared
ctx.sort_windows_by_visual_position = sort_windows_by_visual_position
ctx.grouping_direction = grouping_direction
ctx.grouping_directions = grouping_directions
ctx.workspace_window_count = workspace_window_count
ctx.find_empty_workspace = find_empty_workspace
end
return M

View File

@@ -0,0 +1,48 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
hl.on("hyprland.start", function()
apply_nstack_config()
apply_hyprexpo_config()
apply_hyprwinview_config()
apply_rules()
hl.exec_cmd("sh -lc '/run/current-system/sw/bin/uwsm finalize HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE XAUTHORITY IMALISON_SESSION_TYPE=wayland IMALISON_WINDOW_MANAGER=hyprland || dbus-update-activation-environment --systemd XDG_RUNTIME_DIR WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE IMALISON_WINDOW_MANAGER; systemctl --user start hyprland-session.target'")
hl.exec_cmd("hypridle")
hl.exec_cmd("wl-paste --type text --watch cliphist store")
hl.exec_cmd("wl-paste --type image --watch cliphist store")
write_layout_state()
schedule_nstack_count_update()
refresh_monitor_reserved_cache(0.25)
refresh_monitor_reserved_cache(1.25)
end)
hl.on("config.reloaded", apply_nstack_config)
hl.on("config.reloaded", apply_hyprexpo_config)
hl.on("config.reloaded", apply_hyprwinview_config)
hl.on("config.reloaded", apply_rules)
hl.on("config.reloaded", refresh_shell_workarea_and_scratchpads)
hl.on("layer.opened", refresh_shell_workarea_and_scratchpads)
hl.on("layer.closed", refresh_shell_workarea_and_scratchpads)
hl.on("monitor.added", refresh_shell_workarea_and_scratchpads)
hl.on("monitor.removed", refresh_shell_workarea_and_scratchpads)
hl.on("monitor.layout_changed", refresh_shell_workarea_and_scratchpads)
hl.on("window.open", schedule_nstack_count_update)
hl.on("window.destroy", schedule_nstack_count_update)
hl.on("window.kill", schedule_nstack_count_update)
hl.on("window.move_to_workspace", schedule_nstack_count_update)
hl.on("workspace.active", sync_layout_for_active_workspace)
hl.on("monitor.focused", sync_layout_for_active_workspace)
hl.on("window.open", update_monocle_notice)
hl.on("window.destroy", update_monocle_notice)
hl.on("window.kill", update_monocle_notice)
hl.on("window.move_to_workspace", update_monocle_notice)
hl.on("window.open", adopt_matching_scratchpad_window)
hl.on("window.class", adopt_matching_scratchpad_window)
hl.on("window.title", adopt_matching_scratchpad_window)
end
return M

View File

@@ -0,0 +1,596 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function is_nstack_layout(layout)
return layout == columns_layout or layout == grid_layout
end
local function hyprland_layout(layout)
if layout == grid_layout then
return columns_layout
end
return layout
end
local function update_nstack_count()
if not enable_nstack or not is_nstack_layout(current_layout) then
return
end
local workspace = hl.get_active_workspace()
local count = tiled_window_count(workspace)
if count == 0 then
return
end
local stack_count = count
if current_layout == grid_layout then
stack_count = math.ceil(math.sqrt(count))
end
stack_count = math.max(stack_count, 2)
dispatch(hl.dsp.layout("setstackcount " .. tostring(stack_count)))
end
local function schedule_nstack_count_update()
if stack_update_timer then
stack_update_timer:set_enabled(false)
end
stack_update_timer = hl.timer(update_nstack_count, { timeout = 25, type = "oneshot" })
end
local function dismiss_monocle_notice()
if monocle_notice and monocle_notice:is_alive() then
monocle_notice:dismiss()
end
monocle_notice = nil
end
local function update_monocle_notice()
if current_layout ~= monocle_layout then
dismiss_monocle_notice()
return
end
local workspace = hl.get_active_workspace()
local count = tiled_window_count(workspace)
if count <= 1 then
dismiss_monocle_notice()
return
end
local text = "Monocle: " .. tostring(count) .. " windows"
if monocle_notice and monocle_notice:is_alive() then
monocle_notice:set_text(text)
monocle_notice:set_timeout(60000)
monocle_notice:pause()
else
monocle_notice = hl.notification.create({
text = text,
duration = 60000,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
monocle_notice:pause()
end
end
local function layout_name(layout)
return layout_names[layout] or tostring(layout)
end
local function notify_layout(layout)
hl.notification.create({
text = "Layout: " .. layout_name(layout),
duration = 1200,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function set_layout(layout)
workspace_layouts[workspace_key()] = layout
current_layout = layout
hl.config({ general = { layout = hyprland_layout(layout) } })
write_layout_state()
if is_nstack_layout(layout) then
dismiss_monocle_notice()
schedule_nstack_count_update()
else
update_monocle_notice()
end
end
_G.im_hyprland_set_layout = function(layout)
if not layout_names[layout] then
hl.notification.create({
text = "Unknown layout: " .. tostring(layout),
duration = 1800,
icon = notification_icons.warning,
color = "rgba(edb443ff)",
font_size = 13,
})
return
end
set_layout(layout)
notify_layout(layout)
end
local function sync_layout_for_active_workspace()
current_layout = current_workspace_layout()
hl.config({ general = { layout = hyprland_layout(current_layout) } })
write_layout_state()
if is_nstack_layout(current_layout) then
dismiss_monocle_notice()
schedule_nstack_count_update()
else
update_monocle_notice()
end
end
local function cycle_layout(delta)
local current_index = 1
for index, layout in ipairs(layout_cycle) do
if layout == current_layout then
current_index = index
break
end
end
local next_index = ((current_index - 1 + delta) % #layout_cycle) + 1
local next_layout = layout_cycle[next_index]
set_layout(next_layout)
notify_layout(next_layout)
end
local function toggle_columns_monocle()
if current_layout == columns_layout then
set_layout(monocle_layout)
else
set_layout(columns_layout)
end
end
local function active_group_size()
local window = hl.get_active_window()
return window and window.group and window.group.size or 0
end
local function monocle_next()
local window = hl.get_active_window()
if window and window.group and window.group.size and window.group.size > 1 then
dispatch(hl.dsp.group.next({ window = window_selector(window) }))
elseif current_layout == monocle_layout then
dispatch(hl.dsp.layout("cyclenext"))
update_monocle_notice()
else
dispatch(hl.dsp.window.cycle_next({ next = true, tiled = true, floating = false }))
end
end
local function monocle_prev()
local window = hl.get_active_window()
if window and window.group and window.group.size and window.group.size > 1 then
dispatch(hl.dsp.group.prev({ window = window_selector(window) }))
elseif current_layout == monocle_layout then
dispatch(hl.dsp.layout("cycleprev"))
update_monocle_notice()
else
dispatch(hl.dsp.window.cycle_next({ next = false, tiled = true, floating = false }))
end
end
local function focus_direction(direction)
overview_trace("focus_direction " .. direction)
if active_group_size() > 1 or current_layout == monocle_layout then
if direction == "up" or direction == "left" then
monocle_prev()
else
monocle_next()
end
return
end
dispatch(hl.dsp.focus({ direction = direction }))
end
local function swap_direction(direction)
if enable_nstack and is_nstack_layout(current_layout) and active_group_size() <= 1 then
dispatch(hl.dsp.layout("swapdirection " .. direction))
return
end
dispatch(hl.dsp.window.swap({ direction = direction }))
end
local function focus_workspace(workspace_id)
dispatch(hl.dsp.focus({ workspace = tostring(workspace_id), on_current_monitor = true }))
end
local function move_window_to_workspace(workspace_id, follow, window)
local target_window = window or hl.get_active_window()
local target_selector = window_selector(target_window)
dispatch(hl.dsp.window.move({ workspace = tostring(workspace_id), follow = false, window = target_selector }))
if follow then
focus_workspace(workspace_id)
if target_selector then
dispatch(hl.dsp.focus({ window = target_selector }))
end
end
end
local function notify_tabbed_group(text)
hl.notification.create({
text = text,
duration = 1800,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function active_workspace_tiled_group_candidates(workspace)
local candidates = tiled_windows(workspace)
sort_windows_by_focus_history(candidates)
return candidates
end
local function move_window_into_group(window, anchor)
local selector = window_selector(window)
if not selector then
return false
end
for _, direction in ipairs(grouping_directions(window, anchor)) do
dispatch(hl.dsp.focus({ window = selector }))
dispatch(hl.dsp.window.move({ into_group = direction, window = selector }))
local active = hl.get_active_window()
if active and active.group and active.group.size and active.group.size > 1 then
return true
end
end
return false
end
local function find_tabbed_group_anchor(state)
local active = hl.get_active_window()
if active and active.group and active.group.size and active.group.size > 1 then
return active
end
if not state then
return nil
end
for _, window in ipairs(hl.get_windows()) do
if window and window.address == state.anchor and window.group and window.group.size and window.group.size > 1 then
return window
end
end
return nil
end
local function ordered_windows_for_tabbed_group_restore(state, workspace_id)
local ordered = {}
local seen = {}
local live_windows = windows_by_address()
local workspace = workspace_id and hl.get_workspace(tostring(workspace_id)) or active_workspace()
if state and state.order then
for _, address in ipairs(state.order) do
local window = live_windows[address]
if window and not window.floating and not window.hidden and (not workspace or same_workspace(window.workspace, workspace)) then
ordered[#ordered + 1] = window
seen[address] = true
end
end
end
if workspace then
for _, window in ipairs(tiled_windows(workspace)) do
if window and window.address and not seen[window.address] then
ordered[#ordered + 1] = window
seen[window.address] = true
end
end
end
return ordered
end
local function restore_tabbed_group_window_order(state, workspace_id)
local ordered = ordered_windows_for_tabbed_group_restore(state, workspace_id)
if #ordered <= 1 or not workspace_id then
return
end
local restore_workspace = tabbed_group_restore_workspace_prefix .. tostring(workspace_id)
for _, window in ipairs(ordered) do
move_window_to_workspace(restore_workspace, false, window)
end
for _, window in ipairs(ordered) do
move_window_to_workspace(workspace_id, false, window)
end
end
local function restore_workspace_tabbed_group()
local key = workspace_key()
local state = tabbed_workspace_groups[key]
local anchor = find_tabbed_group_anchor(state)
local anchor_selector = window_selector(anchor)
local target_workspace_id = anchor and anchor.workspace and anchor.workspace.id
if not anchor_selector then
tabbed_workspace_groups[key] = nil
set_layout(columns_layout)
notify_tabbed_group("No tabbed group to restore")
return
end
dispatch(hl.dsp.focus({ window = anchor_selector }))
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
tabbed_workspace_groups[key] = nil
set_layout(columns_layout)
restore_tabbed_group_window_order(state, target_workspace_id)
dispatch(hl.dsp.focus({ window = anchor_selector }))
schedule_nstack_count_update()
end
local function gather_workspace_into_tabbed_group()
local workspace = active_workspace()
if not is_normal_workspace(workspace) then
return
end
local key = workspace_key(workspace)
if tabbed_workspace_groups[key] or active_group_size() > 1 then
restore_workspace_tabbed_group()
return
end
local original_windows = tiled_windows(workspace)
sort_windows_by_visual_position(original_windows)
local original_order = window_address_list(original_windows)
local candidates = active_workspace_tiled_group_candidates(workspace)
if #candidates <= 1 then
set_layout(columns_layout)
return
end
local candidate_addresses = window_address_set(candidates)
local focused = hl.get_active_window()
local anchor = nil
if focused and not focused.floating and not focused.group and window_address_in_set(focused, candidate_addresses) then
anchor = focused
end
if not anchor then
for _, window in ipairs(candidates) do
if not window.group then
anchor = window
break
end
end
end
local anchor_selector = window_selector(anchor)
if not anchor_selector then
notify_tabbed_group("Current tiled windows are already grouped")
return
end
set_layout(columns_layout)
dispatch(hl.dsp.focus({ window = anchor_selector }))
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
local group_windows = {}
for _, window in ipairs(candidates) do
if window ~= anchor and not window.group then
group_windows[#group_windows + 1] = window
end
end
local anchor_x, anchor_y = window_center(anchor)
table.sort(group_windows, function(left, right)
return window_distance_squared(left, anchor_x, anchor_y) < window_distance_squared(right, anchor_x, anchor_y)
end)
local grouped_count = 1
for _, window in ipairs(group_windows) do
if move_window_into_group(window, anchor) then
grouped_count = grouped_count + 1
end
end
if grouped_count <= 1 then
dispatch(hl.dsp.focus({ window = anchor_selector }))
dispatch(hl.dsp.group.toggle({ window = anchor_selector }))
notify_tabbed_group("Unable to group tiled windows")
return
elseif grouped_count < #candidates then
notify_tabbed_group("Grouped " .. tostring(grouped_count) .. " of " .. tostring(#candidates) .. " tiled windows")
end
tabbed_workspace_groups[key] = {
anchor = anchor.address,
order = original_order,
windows = candidate_addresses,
}
dispatch(hl.dsp.focus({ window = anchor_selector }))
end
local function force_columns_layout()
if active_group_size() > 1 or tabbed_workspace_groups[workspace_key()] then
restore_workspace_tabbed_group()
else
set_layout(columns_layout)
end
end
local function cycle_layout_or_restore_tabbed_group()
if active_group_size() > 1 or tabbed_workspace_groups[workspace_key()] then
restore_workspace_tabbed_group()
return
end
cycle_layout(1)
end
local function copy_windows(workspace)
local windows = {}
if not workspace then
return windows
end
for _, window in ipairs(hl.get_workspace_windows(workspace)) do
if window and not window.hidden then
windows[#windows + 1] = window
end
end
return windows
end
local function swap_current_workspace_with(target_id)
local current = active_workspace()
if not current or not current.id or current.id == target_id then
return
end
local target = hl.get_workspace(tostring(target_id))
local current_windows = copy_windows(current)
local target_windows = copy_windows(target)
for _, window in ipairs(current_windows) do
move_window_to_workspace(target_id, false, window)
end
for _, window in ipairs(target_windows) do
move_window_to_workspace(current.id, false, window)
end
focus_workspace(current.id)
end
local function enter_workspace_swap_mode()
hl.notification.create({
text = "Swap with workspace 1-9",
duration = 2200,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
dispatch(hl.dsp.submap("swap-workspace"))
end
local function focus_next_empty_workspace()
local workspace_id = find_empty_workspace(hl.get_active_monitor(), active_workspace_id())
if workspace_id then
focus_workspace(workspace_id)
end
end
local function move_to_next_empty_workspace(follow)
local window = hl.get_active_window()
if not window then
return
end
local workspace_id = find_empty_workspace(hl.get_active_monitor(), active_workspace_id())
if workspace_id then
move_window_to_workspace(workspace_id, follow, window)
end
end
local function cycle_workspace(delta)
local current = active_workspace_id()
local next_workspace = ((current - 1 + delta) % max_workspace) + 1
focus_workspace(next_workspace)
end
local function move_window_to_monitor(direction, follow)
local window = hl.get_active_window()
if not window then
return
end
local original_monitor = hl.get_active_monitor()
dispatch(hl.dsp.window.move({ monitor = direction, follow = follow, window = window_selector(window) }))
if not follow and original_monitor then
dispatch(hl.dsp.focus({ monitor = original_monitor }))
end
end
local function move_window_to_empty_workspace_on_monitor(direction)
local window = hl.get_active_window()
local original_monitor = hl.get_active_monitor()
local target_monitor = hl.get_monitor(direction)
if not window or not original_monitor or not target_monitor or target_monitor == original_monitor then
return
end
local workspace_id = find_empty_workspace(target_monitor, active_workspace_id())
if not workspace_id then
return
end
dispatch(hl.dsp.focus({ monitor = target_monitor }))
focus_workspace(workspace_id)
dispatch(hl.dsp.focus({ monitor = original_monitor }))
move_window_to_workspace(workspace_id, false, window)
end
ctx.is_nstack_layout = is_nstack_layout
ctx.hyprland_layout = hyprland_layout
ctx.update_nstack_count = update_nstack_count
ctx.schedule_nstack_count_update = schedule_nstack_count_update
ctx.dismiss_monocle_notice = dismiss_monocle_notice
ctx.update_monocle_notice = update_monocle_notice
ctx.layout_name = layout_name
ctx.notify_layout = notify_layout
ctx.set_layout = set_layout
ctx.sync_layout_for_active_workspace = sync_layout_for_active_workspace
ctx.cycle_layout = cycle_layout
ctx.toggle_columns_monocle = toggle_columns_monocle
ctx.active_group_size = active_group_size
ctx.monocle_next = monocle_next
ctx.monocle_prev = monocle_prev
ctx.focus_direction = focus_direction
ctx.swap_direction = swap_direction
ctx.focus_workspace = focus_workspace
ctx.move_window_to_workspace = move_window_to_workspace
ctx.notify_tabbed_group = notify_tabbed_group
ctx.active_workspace_tiled_group_candidates = active_workspace_tiled_group_candidates
ctx.move_window_into_group = move_window_into_group
ctx.find_tabbed_group_anchor = find_tabbed_group_anchor
ctx.ordered_windows_for_tabbed_group_restore = ordered_windows_for_tabbed_group_restore
ctx.restore_tabbed_group_window_order = restore_tabbed_group_window_order
ctx.restore_workspace_tabbed_group = restore_workspace_tabbed_group
ctx.gather_workspace_into_tabbed_group = gather_workspace_into_tabbed_group
ctx.force_columns_layout = force_columns_layout
ctx.cycle_layout_or_restore_tabbed_group = cycle_layout_or_restore_tabbed_group
ctx.copy_windows = copy_windows
ctx.swap_current_workspace_with = swap_current_workspace_with
ctx.enter_workspace_swap_mode = enter_workspace_swap_mode
ctx.focus_next_empty_workspace = focus_next_empty_workspace
ctx.move_to_next_empty_workspace = move_to_next_empty_workspace
ctx.cycle_workspace = cycle_workspace
ctx.move_window_to_monitor = move_window_to_monitor
ctx.move_window_to_empty_workspace_on_monitor = move_window_to_empty_workspace_on_monitor
end
return M

View File

@@ -0,0 +1,196 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
if enable_nstack then
hl.plugin.load("/run/current-system/sw/lib/libhyprNStack.so")
end
if enable_hyprexpo and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprexpo.so")
end
if enable_hyprwinview and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhyprwinview.so")
end
if enable_workspace_history and not verify_config then
hl.plugin.load("/run/current-system/sw/lib/libhypr-workspace-history.so")
end
hl.env("XCURSOR_SIZE", "24")
hl.env("HYPRCURSOR_SIZE", "24")
hl.env("QT_QPA_PLATFORMTHEME", "qt5ct")
hl.env("HYPR_MAX_WORKSPACE", "9")
hl.config({
input = {
kb_layout = "us",
kb_variant = "",
kb_model = "",
kb_options = "",
kb_rules = "",
follow_mouse = 1,
sensitivity = 0,
touchpad = {
natural_scroll = false,
},
},
cursor = {
persistent_warps = true,
},
general = {
gaps_in = 5,
gaps_out = 10,
border_size = 2,
col = {
active_border = { colors = { "rgba(3b82f6ee)", "rgba(33ccffee)" }, angle = 45 },
inactive_border = "rgba(00000000)",
},
layout = columns_layout,
allow_tearing = false,
},
decoration = {
rounding = 5,
blur = {
enabled = true,
size = 3,
passes = 1,
},
active_opacity = 1.0,
inactive_opacity = 0.9,
},
animations = {
enabled = true,
},
binds = {
allow_workspace_cycles = true,
workspace_back_and_forth = true,
},
group = {
group_on_movetoworkspace = false,
col = {
border_active = "rgba(edb443ff)",
border_inactive = "rgba(091f2eff)",
},
groupbar = {
enabled = true,
blur = true,
font_size = 13,
gradients = true,
height = 26,
indicator_gap = 0,
indicator_height = 1,
rounding = 5,
gradient_rounding = 5,
text_padding = 8,
col = {
active = "rgba(edb443ff)",
inactive = "rgba(101820f2)",
},
text_color = "rgba(091018ff)",
text_color_inactive = "rgba(f2f5f7ff)",
},
},
misc = {
force_default_wallpaper = 0,
disable_hyprland_logo = true,
exit_window_retains_fullscreen = true,
},
})
hl.curve("overshoot", { type = "bezier", points = { { 0.05, 0.9 }, { 0.1, 1.1 } } })
hl.curve("smoothOut", { type = "bezier", points = { { 0.36, 1 }, { 0.3, 1 } } })
hl.curve("smoothInOut", { type = "bezier", points = { { 0.42, 0 }, { 0.58, 1 } } })
hl.curve("linear", { type = "bezier", points = { { 0, 0 }, { 1, 1 } } })
local animations = {
{ leaf = "global", enabled = true, speed = 8, bezier = "default" },
{ leaf = "windows", enabled = true, speed = 6, bezier = "overshoot", style = "gnomed" },
{ leaf = "windowsIn", enabled = true, speed = 6, bezier = "overshoot", style = "gnomed" },
{ leaf = "windowsOut", enabled = true, speed = 5, bezier = "smoothInOut", style = "gnomed" },
{ leaf = "windowsMove", enabled = true, speed = 6, bezier = "smoothOut" },
{ leaf = "border", enabled = false },
{ leaf = "borderangle", enabled = false },
{ leaf = "fade", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeSwitch", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeShadow", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeGlow", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeDim", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayers", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayersIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeLayersOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopups", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopupsIn", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadePopupsOut", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "fadeDpms", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "layers", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "layersIn", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "layersOut", enabled = true, speed = 5, bezier = "smoothOut", style = "fade" },
{ leaf = "workspaces", enabled = true, speed = 6, bezier = "smoothOut", style = "slidefade 15%" },
{ leaf = "workspacesIn", enabled = true, speed = 6, bezier = "smoothOut", style = "slidefade 15%" },
{ leaf = "workspacesOut", enabled = true, speed = 6, bezier = "smoothOut", style = "slidefade 15%" },
{ leaf = "specialWorkspace", enabled = true, speed = 6, bezier = "smoothOut", style = "slidevert" },
{ leaf = "specialWorkspaceIn", enabled = true, speed = 6, bezier = "smoothOut", style = "slidevert" },
{ leaf = "specialWorkspaceOut", enabled = true, speed = 6, bezier = "smoothOut", style = "slidevert" },
{ leaf = "zoomFactor", enabled = true, speed = 7, bezier = "smoothOut" },
-- Disabled for now: Hyprland 0.54.0 can crash while damaging a monitor
-- from this startup animation's update callback during output discovery.
-- { leaf = "monitorAdded", enabled = true, speed = 5, bezier = "smoothOut" },
{ leaf = "monitorAdded", enabled = false, speed = 5, bezier = "smoothOut" },
}
for _, animation in ipairs(animations) do
hl.animation(animation)
end
local function apply_rules()
if verify_config then
return
end
hl.workspace_rule({ workspace = "w[tv1]s[false]", gaps_out = 0, gaps_in = 0 })
hl.workspace_rule({ workspace = "f[1]s[false]", gaps_out = 0, gaps_in = 0 })
hl.window_rule({ match = { class = "^()$", title = "^()$" }, float = true })
hl.window_rule({ match = { title = "^(Picture-in-Picture)$" }, float = true })
hl.window_rule({ match = { title = "^(Open File)$" }, float = true })
hl.window_rule({ match = { title = "^(Save File)$" }, float = true })
hl.window_rule({ match = { title = "^(Confirm)$" }, float = true })
hl.window_rule({
match = { class = "^(com\\.mitchellh\\.ghostty\\.dropdown)$" },
animation = "slide top",
})
hl.window_rule({
match = { class = "^(.*[Rr]umno.*)$" },
float = true,
pin = true,
center = true,
decorate = false,
no_shadow = true,
})
hl.window_rule({
match = { title = "^(.*[Rr]umno.*)$" },
float = true,
pin = true,
center = true,
decorate = false,
no_shadow = true,
})
hl.window_rule({
name = "subtle-pinned-window-border",
match = { pin = true },
border_size = 2,
border_color = "rgba(edb443ff) rgba(ff4d5dcc)",
})
end
ctx.apply_rules = apply_rules
end
return M

View File

@@ -0,0 +1,105 @@
local shell_ui_command = "hypr_shell_ui"
local columns_layout = "nStack"
local large_main_layout = "master"
local grid_layout = "grid"
local monocle_layout = "monocle"
return {
main_mod = "SUPER",
mod_alt = "SUPER + ALT",
hyper = "SUPER + CTRL + ALT",
terminal = "ghostty --gtk-single-instance=false",
shell_ui_command = shell_ui_command,
launcher_command = shell_ui_command .. " launcher",
run_menu = shell_ui_command .. " run",
-- Hyprland shadows ordinary keybinds after one fires; without transparent,
-- the first overview chord after a focus-moving bind can be skipped.
overview_bind_opts = { dont_inhibit = true, transparent = true },
overview_trace_enabled_path = "/tmp/hypr-overview-bind.enable",
overview_trace_path = "/tmp/hypr-overview-bind.log",
notification_icons = {
warning = 0,
info = 1,
hint = 2,
error = 3,
confused = 4,
ok = 5,
none = 6,
},
max_workspace = 9,
scratchpad_size_ratio = 0.95,
dropdown_height_ratio = 0.5,
columns_layout = columns_layout,
large_main_layout = large_main_layout,
grid_layout = grid_layout,
monocle_layout = monocle_layout,
layout_cycle = { columns_layout, large_main_layout, grid_layout },
layout_names = {
[columns_layout] = "Columns",
[large_main_layout] = "Large main",
[grid_layout] = "Grid",
[monocle_layout] = "Monocle",
},
minimized_workspace = "special:minimized",
tabbed_group_restore_workspace_prefix = "special:tabbed-monocle-restore-",
current_layout = columns_layout,
enable_nstack = true,
enable_hyprexpo = true,
enable_hyprwinview = true,
enable_workspace_history = true,
configure_nstack_plugin_from_lua = false,
workspace_layouts = {},
minimized_windows = {},
tabbed_workspace_groups = {},
window_picker_mode = nil,
window_picker_candidates = {},
stack_update_timer = 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,
},
},
}

View File

@@ -0,0 +1,801 @@
local M = {}
function M.setup(ctx)
local _ENV = ctx
local function same_class_windows(class_name)
local windows = {}
if not class_name or class_name == "" then
return windows
end
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) and window.class == class_name then
windows[#windows + 1] = window
end
end
return windows
end
local function short_text(value, limit)
value = tostring(value or "")
value = value:gsub("[%c\t\r\n]", " ")
if #value <= limit then
return value
end
return value:sub(1, limit - 3) .. "..."
end
local function normal_windows()
local windows = {}
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) then
windows[#windows + 1] = window
end
end
table.sort(windows, function(left, right)
local left_workspace = left.workspace and left.workspace.id or max_workspace + 1
local right_workspace = right.workspace and right.workspace.id or max_workspace + 1
if left_workspace ~= right_workspace then
return left_workspace < right_workspace
end
return (left.focus_history_id or 0) < (right.focus_history_id or 0)
end)
return windows
end
local function window_picker_entry(index, window)
local workspace = window.workspace and window.workspace.id or "?"
local class = short_text(window.class, 18)
local title = short_text(window.title, 48)
return tostring(index) .. " [" .. tostring(workspace) .. "] " .. class .. " " .. title
end
local function remove_minimized_window(target)
local remaining = {}
local target_address = target and target.address
for _, window in ipairs(minimized_windows) do
if window and window.address ~= target_address then
remaining[#remaining + 1] = window
end
end
minimized_windows = remaining
end
local function add_minimized_window(window)
if not window or not window.address then
return
end
remove_minimized_window(window)
minimized_windows[#minimized_windows + 1] = window
end
local function hydrate_minimized_windows()
local by_address = {}
local current_by_address = {}
local hydrated = {}
for _, window in ipairs(hl.get_windows()) do
if window and window.address then
current_by_address[window.address] = window
end
end
for _, window in ipairs(minimized_windows) do
local current = window and window.address and current_by_address[window.address]
if current and is_minimized_window(current) and not by_address[current.address] then
by_address[current.address] = true
hydrated[#hydrated + 1] = current
end
end
for _, window in pairs(current_by_address) do
if window and window.address and is_minimized_window(window) and not by_address[window.address] then
by_address[window.address] = true
hydrated[#hydrated + 1] = window
end
end
minimized_windows = hydrated
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 geometry = tiled_window_geometry(hl.get_active_window())
dispatch(hl.dsp.window.float({ action = "enable", window = geometry and geometry.selector or nil }))
if geometry then
dispatch(hl.dsp.window.resize({ x = geometry.width, y = geometry.height, relative = false, window = geometry.selector }))
dispatch(hl.dsp.window.move({ x = geometry.x, y = geometry.y, relative = false, window = geometry.selector }))
end
return geometry
end
local function float_and_drag_active_window()
float_active_window_preserving_tiled_geometry()
dispatch(hl.dsp.window.drag())
end
local function float_and_resize_active_window()
float_active_window_preserving_tiled_geometry()
dispatch(hl.dsp.window.resize())
end
local function toggle_pinned_active_window()
local window = hl.get_active_window()
local selector = window_selector(window)
if not window or not selector then
return
end
if window.pinned then
dispatch(hl.dsp.window.pin({ action = "disable", window = selector }))
dispatch(hl.dsp.window.float({ action = "disable", window = selector }))
return
end
if not window.floating then
float_active_window_preserving_tiled_geometry()
end
dispatch(hl.dsp.window.pin({ action = "enable", window = selector }))
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()
hydrate_minimized_windows()
local windows = {}
for _, window in ipairs(minimized_windows) do
if window and window.address and is_minimized_window(window) then
windows[#windows + 1] = window
end
end
minimized_windows = windows
return windows
end
local function restore_minimized_window(window, workspace)
if not window or not workspace then
return false
end
move_window_to_workspace(workspace.id, false, window)
return true
end
local function window_picker_candidates_for(mode)
if mode == "minimized" then
return current_minimized_windows()
end
local focused = hl.get_active_window()
local workspace = active_workspace()
local candidates = {}
for _, window in ipairs(normal_windows()) do
local include = true
if mode == "bring" and workspace and window.workspace == workspace then
include = false
elseif mode == "replace" and focused and window == focused then
include = false
end
if include then
candidates[#candidates + 1] = window
end
end
return candidates
end
local function activate_window_picker_candidate(index)
local window = window_picker_candidates[index]
local mode = window_picker_mode
window_picker_mode = nil
window_picker_candidates = {}
dispatch(hl.dsp.submap("reset"))
if not window then
return
end
if mode == "go" then
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
local workspace = active_workspace()
if mode == "bring" and workspace then
move_window_to_workspace(workspace.id, false, window)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
if mode == "minimized" and workspace then
remove_minimized_window(window)
restore_minimized_window(window, workspace)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
if mode == "replace" then
local focused = hl.get_active_window()
if focused and focused ~= window then
dispatch(hl.dsp.window.swap({ target = window_selector(window), window = window_selector(focused) }))
dispatch(hl.dsp.focus({ window = window_selector(window) }))
end
end
end
local function enter_window_picker(mode)
window_picker_mode = mode
window_picker_candidates = window_picker_candidates_for(mode)
if #window_picker_candidates == 0 then
local empty_text = "No windows available"
if mode == "minimized" then
empty_text = "No minimized windows"
end
hl.notification.create({
text = empty_text,
duration = 1800,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
return
end
local lines = {}
local count = math.min(#window_picker_candidates, 9)
for i = 1, count do
lines[#lines + 1] = window_picker_entry(i, window_picker_candidates[i])
end
hl.notification.create({
text = table.concat(lines, "\n"),
duration = 5000,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 11,
})
dispatch(hl.dsp.submap("window-picker"))
end
local function gather_focused_class()
local focused = hl.get_active_window()
local workspace = active_workspace()
if not focused or not workspace or not focused.class or focused.class == "" then
return
end
local count = 0
for _, window in ipairs(same_class_windows(focused.class)) do
if window ~= focused and window.workspace ~= workspace then
move_window_to_workspace(workspace.id, false, window)
count = count + 1
end
end
hl.notification.create({
text = "Gathered " .. tostring(count) .. " " .. focused.class .. " windows",
duration = 1600,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
end
local function focus_next_class()
local focused = hl.get_active_window()
if not focused or not focused.class or focused.class == "" then
dispatch(hl.dsp.window.cycle_next({ next = true, tiled = true, floating = false }))
return
end
local classes = {}
local first_by_class = {}
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) and window.class and window.class ~= "" and not first_by_class[window.class] then
first_by_class[window.class] = window
classes[#classes + 1] = window.class
end
end
table.sort(classes)
if #classes <= 1 then
return
end
local current_index = 1
for index, class_name in ipairs(classes) do
if class_name == focused.class then
current_index = index
break
end
end
local next_class = classes[(current_index % #classes) + 1]
local target = first_by_class[next_class]
if target then
dispatch(hl.dsp.focus({ window = window_selector(target) }))
end
end
local function show_active_window_info()
local window = hl.get_active_window()
if not window then
hl.notification.create({
text = "No active window",
duration = 1800,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 13,
})
return
end
local workspace = window.workspace and (window.workspace.name or window.workspace.id) or "?"
local lines = {
"Class: " .. tostring(window.class or ""),
"Title: " .. tostring(window.title or ""),
"Workspace: " .. tostring(workspace),
"Pinned: " .. tostring(window.pinned or false),
"Address: " .. tostring(window.address or ""),
"PID: " .. tostring(window.pid or ""),
}
hl.notification.create({
text = table.concat(lines, "\n"),
duration = 5000,
icon = notification_icons.info,
color = "rgba(edb443ff)",
font_size = 11,
})
end
local function raise_or_spawn(class_fragment, command)
local fragment = string.lower(class_fragment)
for _, window in ipairs(hl.get_windows()) do
if is_normal_window(window) and window.class and string.find(string.lower(window.class), fragment, 1, true) then
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
end
hl.exec_cmd(command)
end
local function minimize_active_window()
local window = hl.get_active_window()
if not window then
return
end
add_minimized_window(window)
move_window_to_workspace(minimized_workspace, false, window)
end
local function restore_last_minimized()
local workspace = active_workspace()
if not workspace then
return
end
hydrate_minimized_windows()
while #minimized_windows > 0 do
local window = table.remove(minimized_windows)
if window and window.address and is_minimized_window(window) then
restore_minimized_window(window, workspace)
dispatch(hl.dsp.focus({ window = window_selector(window) }))
return
end
end
end
local function restore_all_minimized()
local workspace = active_workspace()
if not workspace then
return
end
hydrate_minimized_windows()
while #minimized_windows > 0 do
restore_minimized_window(table.remove(minimized_windows), workspace)
end
end
local function minimize_other_classes()
local focused = hl.get_active_window()
local workspace = active_workspace()
if not focused or not workspace then
return
end
for _, window in ipairs(tiled_windows(workspace)) do
if window ~= focused and window.class ~= focused.class then
add_minimized_window(window)
move_window_to_workspace(minimized_workspace, false, window)
end
end
end
local function restore_focused_class()
local focused = hl.get_active_window()
local workspace = active_workspace()
if not focused or not workspace or not focused.class then
return
end
hydrate_minimized_windows()
local remaining = {}
for _, window in ipairs(minimized_windows) do
if window and window.class == focused.class and is_minimized_window(window) then
restore_minimized_window(window, workspace)
else
remaining[#remaining + 1] = window
end
end
minimized_windows = remaining
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.short_text = short_text
ctx.normal_windows = normal_windows
ctx.window_picker_entry = window_picker_entry
ctx.remove_minimized_window = remove_minimized_window
ctx.add_minimized_window = add_minimized_window
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_and_drag_active_window = float_and_drag_active_window
ctx.float_and_resize_active_window = float_and_resize_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.restore_minimized_window = restore_minimized_window
ctx.window_picker_candidates_for = window_picker_candidates_for
ctx.activate_window_picker_candidate = activate_window_picker_candidate
ctx.enter_window_picker = enter_window_picker
ctx.gather_focused_class = gather_focused_class
ctx.focus_next_class = focus_next_class
ctx.show_active_window_info = show_active_window_info
ctx.raise_or_spawn = raise_or_spawn
ctx.minimize_active_window = minimize_active_window
ctx.restore_last_minimized = restore_last_minimized
ctx.restore_all_minimized = restore_all_minimized
ctx.minimize_other_classes = minimize_other_classes
ctx.restore_focused_class = restore_focused_class
ctx.toggle_scratchpad = toggle_scratchpad
end
return M

View File

@@ -20,5 +20,5 @@ Avoid dropping scripts in `~/bin` or `~/.local/bin` unless the user explicitly a
- Existing rofi scripts live in `../dotfiles/lib/bin/` (e.g. `rofi_command.sh`). - Existing rofi scripts live in `../dotfiles/lib/bin/` (e.g. `rofi_command.sh`).
- Keybind locations: - Keybind locations:
- Hyprland: `../dotfiles/config/hypr/hyprland.lua` - Hyprland: `../dotfiles/config/hypr/hyprland/binds.lua`
- XMonad: `../dotfiles/config/xmonad/xmonad.hs` - XMonad: `../dotfiles/config/xmonad/xmonad.hs`

View File

@@ -1,20 +1,21 @@
{ {
pkgs, pkgs,
hyprlandConfig, hyprlandConfigDir,
}: }:
pkgs.runCommand "hyprland-config-syntax" { pkgs.runCommand "hyprland-config-syntax" {
nativeBuildInputs = [pkgs.lua5_4]; nativeBuildInputs = [pkgs.lua5_4];
} '' } ''
cp ${hyprlandConfig} hyprland.lua cp -r ${hyprlandConfigDir}/. .
luac -p hyprland.lua chmod -R +w .
luac -p hyprland.lua hyprland/*.lua
if grep -n 'hyprctl' hyprland.lua | grep -v 'hyprctl reload' | grep -v 'hyprctl eval' | grep -v 'hyprctl_eval' | grep -v 'hyprctl -j monitors'; then if grep -Rn 'hyprctl' hyprland.lua hyprland/*.lua | grep -v 'hyprctl reload' | grep -v 'hyprctl eval' | grep -v 'hyprctl_eval' | grep -v 'hyprctl -j monitors'; then
echo "hyprland.lua should not shell out to hyprctl for window/workspace manipulation" >&2 echo "Hyprland Lua config should not shell out to hyprctl for window/workspace manipulation" >&2
exit 1 exit 1
fi fi
if grep -nE 'hl[.]dsp.*[)][(][)]' hyprland.lua; then if grep -RnE 'hl[.]dsp.*[)][(][)]' hyprland.lua hyprland/*.lua; then
echo "hyprland.lua should use hl.dispatch(...) instead of calling dispatcher objects directly" >&2 echo "Hyprland Lua config should use hl.dispatch(...) instead of calling dispatcher objects directly" >&2
exit 1 exit 1
fi fi

View File

@@ -55,15 +55,16 @@ in {
hypr-workspace-history = inputs.hypr-workspace-history.packages.${system}.hypr-workspace-history; hypr-workspace-history = inputs.hypr-workspace-history.packages.${system}.hypr-workspace-history;
hyprland-config-syntax = import ../checks/hyprland-config-syntax { hyprland-config-syntax = import ../checks/hyprland-config-syntax {
inherit pkgs; inherit pkgs;
hyprlandConfig = ../../dotfiles/config/hypr/hyprland.lua; hyprlandConfigDir = ../../dotfiles/config/hypr;
}; };
hyprland-verify-config = let hyprland-verify-config = let
hyprlandPackage = inputs.hyprland.packages.${system}.hyprland; hyprlandPackage = inputs.hyprland.packages.${system}.hyprland;
hyprNStackPackage = inputs.hyprNStack.packages.${system}.hyprNStack; hyprNStackPackage = inputs.hyprNStack.packages.${system}.hyprNStack;
in in
pkgs.runCommand "hyprland-lua-verify-config" {} '' pkgs.runCommand "hyprland-lua-verify-config" {} ''
cp ${../../dotfiles/config/hypr/hyprland.lua} hyprland.lua cp -r ${../../dotfiles/config/hypr}/. .
substituteInPlace hyprland.lua \ chmod -R +w .
substituteInPlace hyprland/settings.lua \
--replace-fail /run/current-system/sw/lib/libhyprNStack.so \ --replace-fail /run/current-system/sw/lib/libhyprNStack.so \
${hyprNStackPackage}/lib/libhyprNStack.so ${hyprNStackPackage}/lib/libhyprNStack.so
export XDG_RUNTIME_DIR="$TMPDIR/runtime" export XDG_RUNTIME_DIR="$TMPDIR/runtime"