diff --git a/README.org b/README.org index fe3b2b38..d1a419ff 100644 --- a/README.org +++ b/README.org @@ -137,8 +137,9 @@ notifications, SNI/tray support, fonts, and app defaults. The currently important pieces are: -- Hyprland configuration in [[file:dotfiles/config/hypr/hyprland.lua][dotfiles/config/hypr/hyprland.lua]], backed by custom - plugin inputs in the NixOS flake. +- Hyprland configuration in [[file:dotfiles/config/hypr/hyprland.lua][dotfiles/config/hypr/hyprland.lua]], with imported Lua + 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= and =xmonad-contrib= available as submodules/checkouts. - Taffybar configuration in [[file:dotfiles/config/taffybar/taffybar.hs][dotfiles/config/taffybar/taffybar.hs]], plus a local diff --git a/dotfiles/config/hypr/hyprland.lua b/dotfiles/config/hypr/hyprland.lua index 0999bcfe..b26f38bb 100644 --- a/dotfiles/config/hypr/hyprland.lua +++ b/dotfiles/config/hypr/hyprland.lua @@ -1,2420 +1,41 @@ -local main_mod = "SUPER" -local mod_alt = "SUPER + ALT" -local hyper = "SUPER + CTRL + ALT" +local function config_dir() + local source = debug.getinfo(1, "S").source + if source:sub(1, 1) == "@" then + source = source:sub(2) + end -local terminal = "ghostty --gtk-single-instance=false" -local shell_ui_command = "hypr_shell_ui" -local launcher_command = shell_ui_command .. " launcher" -local 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. -local overview_bind_opts = { dont_inhibit = true, transparent = true } -local overview_trace_enabled_path = "/tmp/hypr-overview-bind.enable" -local overview_trace_path = "/tmp/hypr-overview-bind.log" -local notification_icons = { - warning = 0, - info = 1, - hint = 2, - error = 3, - confused = 4, - ok = 5, - none = 6, + local dir = source:match("^(.*)/[^/]*$") + if dir and dir ~= "" then + return dir + end + + return "." +end + +local base_dir = config_dir() +package.path = table.concat({ + base_dir .. "/?.lua", + base_dir .. "/?/init.lua", + package.path, +}, ";") + +local modules = { + "hyprland.state", + "hyprland.core", + "hyprland.layouts", + "hyprland.windows", + "hyprland.settings", + "hyprland.binds", + "hyprland.events", } -local max_workspace = 9 -local scratchpad_size_ratio = 0.95 -local dropdown_height_ratio = 0.5 -local columns_layout = "nStack" -local large_main_layout = "master" -local grid_layout = "grid" -local monocle_layout = "monocle" -local layout_cycle = { columns_layout, large_main_layout, grid_layout } -local layout_names = { - [columns_layout] = "Columns", - [large_main_layout] = "Large main", - [grid_layout] = "Grid", - [monocle_layout] = "Monocle", -} -local minimized_workspace = "special:minimized" -local tabbed_group_restore_workspace_prefix = "special:tabbed-monocle-restore-" -local current_layout = columns_layout -local enable_nstack = true -local enable_hyprexpo = true -local enable_hyprwinview = true -local enable_workspace_history = true -local configure_nstack_plugin_from_lua = false -local workspace_layouts = {} -local minimized_windows = {} -local tabbed_workspace_groups = {} -local window_picker_mode = nil -local window_picker_candidates = {} -local stack_update_timer = nil -local monocle_notice = nil -local scratchpad_pending = {} -local monitor_reserved_cache_path = (os.getenv("XDG_RUNTIME_DIR") or "/tmp") .. "/hyprland-monitor-reserved.tsv" -local scratchpad_fallback_reserved_top = 60 - -local 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 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 - -local 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) +for _, module in ipairs(modules) do + package.loaded[module] = nil 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 - -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 +local ctx = require(modules[1]) +setmetatable(ctx, { __index = _G }) - focus_workspace(current.id) +for i = 2, #modules do + require(modules[i]).setup(ctx) 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 - -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 - -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 - -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) - -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) diff --git a/dotfiles/config/hypr/hyprland/binds.lua b/dotfiles/config/hypr/hyprland/binds.lua new file mode 100644 index 00000000..9365448f --- /dev/null +++ b/dotfiles/config/hypr/hyprland/binds.lua @@ -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 diff --git a/dotfiles/config/hypr/hyprland/core.lua b/dotfiles/config/hypr/hyprland/core.lua new file mode 100644 index 00000000..f1131961 --- /dev/null +++ b/dotfiles/config/hypr/hyprland/core.lua @@ -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 diff --git a/dotfiles/config/hypr/hyprland/events.lua b/dotfiles/config/hypr/hyprland/events.lua new file mode 100644 index 00000000..f9ef226a --- /dev/null +++ b/dotfiles/config/hypr/hyprland/events.lua @@ -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 diff --git a/dotfiles/config/hypr/hyprland/layouts.lua b/dotfiles/config/hypr/hyprland/layouts.lua new file mode 100644 index 00000000..a90e04e1 --- /dev/null +++ b/dotfiles/config/hypr/hyprland/layouts.lua @@ -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 diff --git a/dotfiles/config/hypr/hyprland/settings.lua b/dotfiles/config/hypr/hyprland/settings.lua new file mode 100644 index 00000000..de200ffd --- /dev/null +++ b/dotfiles/config/hypr/hyprland/settings.lua @@ -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 diff --git a/dotfiles/config/hypr/hyprland/state.lua b/dotfiles/config/hypr/hyprland/state.lua new file mode 100644 index 00000000..5ec244b4 --- /dev/null +++ b/dotfiles/config/hypr/hyprland/state.lua @@ -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, + }, + }, +} diff --git a/dotfiles/config/hypr/hyprland/windows.lua b/dotfiles/config/hypr/hyprland/windows.lua new file mode 100644 index 00000000..69b4f02e --- /dev/null +++ b/dotfiles/config/hypr/hyprland/windows.lua @@ -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 diff --git a/nixos/AGENTS.md b/nixos/AGENTS.md index 1efe3b53..7ae60487 100644 --- a/nixos/AGENTS.md +++ b/nixos/AGENTS.md @@ -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`). - Keybind locations: - - Hyprland: `../dotfiles/config/hypr/hyprland.lua` + - Hyprland: `../dotfiles/config/hypr/hyprland/binds.lua` - XMonad: `../dotfiles/config/xmonad/xmonad.hs` diff --git a/nixos/checks/hyprland-config-syntax/default.nix b/nixos/checks/hyprland-config-syntax/default.nix index c5e1dadf..6e0730a9 100644 --- a/nixos/checks/hyprland-config-syntax/default.nix +++ b/nixos/checks/hyprland-config-syntax/default.nix @@ -1,20 +1,21 @@ { pkgs, - hyprlandConfig, + hyprlandConfigDir, }: pkgs.runCommand "hyprland-config-syntax" { nativeBuildInputs = [pkgs.lua5_4]; } '' - cp ${hyprlandConfig} hyprland.lua - luac -p hyprland.lua + cp -r ${hyprlandConfigDir}/. . + 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 - echo "hyprland.lua should not shell out to hyprctl for window/workspace manipulation" >&2 + 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 config should not shell out to hyprctl for window/workspace manipulation" >&2 exit 1 fi - if grep -nE 'hl[.]dsp.*[)][(][)]' hyprland.lua; then - echo "hyprland.lua should use hl.dispatch(...) instead of calling dispatcher objects directly" >&2 + if grep -RnE 'hl[.]dsp.*[)][(][)]' hyprland.lua hyprland/*.lua; then + echo "Hyprland Lua config should use hl.dispatch(...) instead of calling dispatcher objects directly" >&2 exit 1 fi diff --git a/nixos/flake/per-system.nix b/nixos/flake/per-system.nix index 11fdeda6..cc4c0ac5 100644 --- a/nixos/flake/per-system.nix +++ b/nixos/flake/per-system.nix @@ -55,15 +55,16 @@ in { hypr-workspace-history = inputs.hypr-workspace-history.packages.${system}.hypr-workspace-history; hyprland-config-syntax = import ../checks/hyprland-config-syntax { inherit pkgs; - hyprlandConfig = ../../dotfiles/config/hypr/hyprland.lua; + hyprlandConfigDir = ../../dotfiles/config/hypr; }; hyprland-verify-config = let hyprlandPackage = inputs.hyprland.packages.${system}.hyprland; hyprNStackPackage = inputs.hyprNStack.packages.${system}.hyprNStack; in pkgs.runCommand "hyprland-lua-verify-config" {} '' - cp ${../../dotfiles/config/hypr/hyprland.lua} hyprland.lua - substituteInPlace hyprland.lua \ + cp -r ${../../dotfiles/config/hypr}/. . + chmod -R +w . + substituteInPlace hyprland/settings.lua \ --replace-fail /run/current-system/sw/lib/libhyprNStack.so \ ${hyprNStackPackage}/lib/libhyprNStack.so export XDG_RUNTIME_DIR="$TMPDIR/runtime"