From 016e0aadfec3b28563e32b57d6725a083e56c04e Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Tue, 28 Apr 2026 12:17:47 -0700 Subject: [PATCH] Implement Hyprland Lua migration --- docs/hyprland-lua-migration-checklist.md | 203 ++++ docs/tiling-wm-experience.md | 11 + dotfiles/config/hypr/hyprland.conf | 12 +- dotfiles/config/hypr/hyprland.lua | 1380 ++++++++++++++++++++++ dotfiles/config/xmonad/xmonad.hs | 6 +- nixos/flake.lock | 247 ++-- nixos/flake.nix | 152 ++- nixos/hyprland.nix | 18 +- nixos/machines/ryzen-shine.nix | 1 + 9 files changed, 1875 insertions(+), 155 deletions(-) create mode 100644 docs/hyprland-lua-migration-checklist.md create mode 100644 dotfiles/config/hypr/hyprland.lua diff --git a/docs/hyprland-lua-migration-checklist.md b/docs/hyprland-lua-migration-checklist.md new file mode 100644 index 00000000..a183f9e5 --- /dev/null +++ b/docs/hyprland-lua-migration-checklist.md @@ -0,0 +1,203 @@ +# Hyprland Lua Migration Checklist + +This checklist tracks the migration described in `docs/tiling-wm-experience.md`. + +Guiding rule for shelling out: + +- Prefer Lua for compositor/window/workspace state changes. +- Avoid `hyprctl` for window manipulation unless there is no usable Lua API. +- `hyprctl` remains acceptable for non-window-control escape hatches such as + `hyprctl reload`. +- External utilities remain acceptable where they are the real tool being + launched, for example rofi, cliphist, grim/slurp/swappy, playerctl, hyprlock, + and systemd commands. + +## 0. Version And Build Base + +- [x] Update/confirm Hyprland Lua input at latest usable upstream target. +- [x] Keep stable Hyprland path intact until Lua path is proven. +- [x] Keep hy3/hyprexpo out of the Lua branch. +- [x] Keep hyprNStack following the Lua Hyprland input. +- [x] Rebuild hyprNStack against the Lua Hyprland branch. +- [x] Keep a cheap Lua check: parse config, execute against stub, reject + `hyprctl` in the Lua config's window/workspace manipulation path. +- [x] Add a real Hyprland Lua verifier check for the config parser path. + +Current upstream note: latest Hyprland release observed during this migration is +`v0.54.3`; the Lua config input tracks PR 13817 and was already at the current +PR head `c35a8a5` dated 2026-04-26. The non-Lua fallback remains pinned to the older +hy3/hyprexpo-compatible stack. + +## 1. Core Layout + +- [x] Primary layout is equal-width columns. +- [x] No scrolling layout. +- [x] No hy3 in Lua path. +- [x] Dynamic redistribution on open/close via Lua-managed nStack count. +- [x] Monocle/tabbed-style layout available. +- [x] Direct jump to columns layout. +- [x] Direct jump to monocle layout. +- [x] Directional focus cycles in monocle. +- [x] Visual indication of hidden monocle windows, currently notification. +- [x] Make layout state per workspace instead of one global current layout. +- [x] Preserve one-window smart gaps in the live config path. +- [x] Use a persistent monocle indicator instead of a transient notification. + +Smart-gaps note: nStack uses `no_gaps_when_only = true`; Hyprland workspace +rules are still applied at runtime for broader parity, but skipped during +`--verify-config` because the current Lua PR segfaults when rule bindings run in +verifier mode. + +## 2. Workspace Behavior + +- [x] `Super+1..9` focuses bounded workspaces. +- [x] `Super+Shift+1..9` sends window without following. +- [x] `Super+Ctrl+1..9` sends and follows. +- [x] Previous workspace per monitor uses Lua-tracked history. +- [x] Implement next empty workspace focus in Lua. +- [x] Implement move focused window to next empty workspace without following. +- [x] Implement move focused window to next empty workspace and follow. +- [x] Implement bounded workspace cycling `1..9` in Lua, replacing + `workspace-scroll.sh`. +- [x] Implement workspace swap or decide whether native dispatcher is enough. +- [x] Track current monitor workspace history explicitly, with native + `previous_per_monitor` as fallback. + +## 3. Directional Navigation + +- [x] `Super+w/a/s/d` focuses windows. +- [x] `Super+Shift+w/a/s/d` swaps windows. +- [x] `Hyper+w/a/s/d` focuses monitors. +- [x] `Hyper+Shift+w/a/s/d` moves windows to monitors. +- [x] `Super+z` next monitor. +- [x] `Super+Shift+z` move to next monitor. +- [x] Replace any old cursor-follow/move scripts fully. +- [x] Add required `Super+Ctrl+w/a/s/d` move-to-monitor behavior preserving + useful focus. +- [x] Add "move to empty workspace on monitor in direction" without requiring + `Hyper+Ctrl`. +- [ ] Verify directional focus in monocle behaves predictably. + +## 4. Script Elimination Priority + +- [x] Core layout switching no longer uses scripts. +- [x] Core column count logic no longer uses scripts or `hyprctl`. +- [x] Replace `find-empty-workspace.sh`. +- [x] Replace `workspace-goto-empty.sh`. +- [x] Replace `workspace-move-to-empty.sh`. +- [x] Replace `workspace-scroll.sh`. +- [x] Replace `cycle-layout.sh`. +- [x] Replace `movewindow-follow-cursor.sh`. +- [x] Replace `gather-class.sh`. +- [x] Replace `focus-next-class.sh`. +- [x] Replace `raise-or-run.sh`. +- [x] Replace minimize scripts if Lua can maintain hidden workspace state. +- [x] Replace `swap-workspaces.sh`. +- [x] Decide whether rofi-backed pickers remain scripts or become + Lua-generated command pipes. Rofi itself remains external. + +## 5. Overview And Window Discovery + +- [x] Replace hyprexpo with the first-pass Lua numbered window picker. +- [x] Implement first-pass `Super+Tab` overview via Lua window picker. +- [x] Implement first-pass `Super+Shift+Tab` bring overview via Lua window picker. +- [x] Implement first-pass Lua-native go-to-window picker. +- [x] Implement first-pass Lua-native bring-window picker. +- [x] Implement first-pass Lua-native replace-window picker. +- [ ] Picker entries include icons. +- [x] Picker entries include title/workspace. +- [x] Hide scratchpad/minimized/internal windows from normal pickers. +- [x] Decide whether picker data generation can be Lua-native with rofi as only + external process. + +Picker decision: current Lua API can query and manipulate windows directly, but +does not expose a synchronous way to run rofi and consume its selected output. +The first pass therefore uses Lua-native numbered submaps and notifications. +A final rofi/icon picker would need either a small IPC bridge or an upstream Lua +process-output/callback primitive. + +## 6. Scratchpads + +- [x] Preserve named scratchpads: element, gmail, htop, messages, slack, + spotify, transmission, volume. +- [x] Preserve dropdown terminal scratchpad. +- [x] Scratchpads near-fullscreen and centered. +- [x] Scratchpads hidden from normal listings/status bar. +- [x] Toggling scratchpad exits fullscreen/monocle state first. +- [x] Decide hyprscratch daemon is not needed in the Lua branch. +- [x] Replace `hyprscratch toggle` with Lua-managed scratchpad toggles. +- [x] Disable hyprscratch service on the Lua branch. +- [x] Handle delayed class/title assignment with window class/title event adoption. +- [x] Handle already-running app. +- [x] Handle minimized app. +- [x] Handle app on another workspace. + +## 7. Minimization + +- [x] Implement minimize active window. +- [x] Implement restore last minimized window. +- [x] Exclude minimized windows from layout. +- [x] Exclude minimized windows from normal go/bring lists. +- [x] Implement minimized picker. +- [x] Implement restore all minimized. +- [x] Implement minimize other windows of current workspace class. +- [x] Implement restore windows of focused class. +- [x] Decide hidden workspace naming/state model for minimized windows. + +## 8. Class-Aware Workflows + +- [x] Gather all windows of focused class onto current workspace. +- [x] Focus next window of different/same class as desired parity. +- [x] Browser raise-or-spawn. +- [x] Window info command exposes class/title/workspace/address/pid. +- [ ] Window menus expose real window icons. +- [x] Prefer Lua window queries over `hyprctl clients`. + +## 9. Status Bar Contract + +- [ ] Confirm taffybar can still list normal workspaces. +- [ ] Confirm special scratchpad/minimize workspaces are filtered. +- [ ] Confirm active workspace per monitor remains visible. +- [ ] Confirm class/title/active/minimized/urgent metadata is available. +- [x] Expose layout name/state if practical. +- [ ] Confirm workspace/window positioning remains enough for icon strips. + +Layout state note: Lua writes `$XDG_RUNTIME_DIR/hyprland-layout-state` with the +active workspace, active layout, and per-workspace layout map. Taffybar still +needs a live readback check. + +## 10. Session And Utilities + +- [x] Terminal binding preserved. +- [x] Launcher/run menu preserved. +- [x] Media keys preserved. +- [x] Clipboard history binding preserved. +- [x] Screenshot binding preserved. +- [x] Lock binding preserved. +- [x] Session startup target integration preserved. +- [x] `hyprctl reload` may remain available as a non-window-manipulation escape + hatch. +- [x] Resolve `Hyper+w` conflict: monitor focus must win; wallpaper picker + needs another key. +- [x] Keep rofi utility commands as external commands unless there is a + meaningful Lua replacement. +- [x] Decide which shell utilities are acceptable because they are not Hyprland + control scripts. + +## 11. Validation + +- [x] Lua syntax check. +- [x] Lua stub execution check. +- [x] `hyprctl` rejection in Lua config for window/workspace manipulation. +- [x] Real `Hyprland --verify-config` check. +- [x] hyprNStack flake build check. +- [x] `ryzen-shine` system dry-run. +- [x] Re-run checks after Hyprland/Lua input confirmation. +- [ ] Try live compositor smoke test again after version bump. +- [x] Document `--verify-config` caveats for Lua rule/plugin-specific config. +- [ ] Eventually run `just switch` only when the branch is coherent enough for a + live test. + +Live-smoke note: this Hyprland binary exposes `--verify-config` but no +`--headless` CLI flag. A true compositor smoke test still needs either a nested +Wayland session that avoids startup side effects or an intentional `just switch`. diff --git a/docs/tiling-wm-experience.md b/docs/tiling-wm-experience.md index cb88cee9..f45833c6 100644 --- a/docs/tiling-wm-experience.md +++ b/docs/tiling-wm-experience.md @@ -74,6 +74,14 @@ Required behavior: - Moving the focused window to an empty workspace on the monitor in a direction remains required behavior, but it should not require an extra `Hyper` modifier beyond `Shift`. +- `Super+w/a/s/d` focuses windows directionally. +- `Super+Shift+w/a/s/d` swaps or moves the focused window directionally. +- `Super+Ctrl+w/a/s/d` moves the focused window to the monitor in that + direction while preserving useful focus. +- `Super+Ctrl+Shift+w/a/s/d` moves the focused window to an empty workspace on + the monitor in that direction. +- `Hyper+w/a/s/d` focuses monitors directionally. +- `Hyper+Shift+w/a/s/d` swaps or moves windows between monitors directionally. - Directional focus in tabbed/fullscreen mode should cycle predictably through windows even though their screen geometry overlaps. @@ -88,6 +96,7 @@ Required behavior: - Tiling is dynamic. - Primary layout is equal-width vertical columns. +- Scrolling layouts are not acceptable. - All ordinary splits are vertical. - Adding windows dynamically redistributes all tiled windows evenly. - Removing windows dynamically redistributes all tiled windows evenly. @@ -235,6 +244,8 @@ Important behavior: Nice behavior: - Wallpaper behavior remains consistent. +- Wallpaper selection uses `Hyper+comma`; `Hyper+w/a/s/d` are reserved for + directional monitor focus. - Idle behavior remains consistent. - Lock behavior remains consistent. - Clipboard history behavior remains consistent. diff --git a/dotfiles/config/hypr/hyprland.conf b/dotfiles/config/hypr/hyprland.conf index f9c850c8..5a7d6050 100644 --- a/dotfiles/config/hypr/hyprland.conf +++ b/dotfiles/config/hypr/hyprland.conf @@ -315,12 +315,12 @@ bind = $hyper SHIFT, S, movewindow, mon:d bind = $hyper SHIFT, A, movewindow, mon:l bind = $hyper SHIFT, D, movewindow, mon:r -# Shift to empty workspace on screen direction (Hyper + Ctrl + WASD) +# Shift to empty workspace on screen direction (Super + Ctrl + Shift + WASD) # Like XMonad's shiftToEmptyOnScreen -bind = $hyper CTRL, W, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh u -bind = $hyper CTRL, S, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh d -bind = $hyper CTRL, A, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh l -bind = $hyper CTRL, D, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh r +bind = $mainMod CTRL SHIFT, W, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh u +bind = $mainMod CTRL SHIFT, S, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh d +bind = $mainMod CTRL SHIFT, A, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh l +bind = $mainMod CTRL SHIFT, D, exec, ~/.config/hypr/scripts/shift-to-empty-on-screen.sh r # ----------------------------------------------------------------------------- # LAYOUT CONTROL (XMonad-like with hy3) @@ -527,7 +527,7 @@ bind = $hyper, 9, exec, start_synergy.sh bind = $hyper, I, exec, rofi_select_input.hs bind = $hyper, backslash, exec, /home/imalison/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle bind = $hyper, O, exec, rofi_paswitch -bind = $hyper, W, exec, rofi_wallpaper.sh +bind = $hyper, comma, exec, rofi_wallpaper.sh bind = $hyper, Y, exec, rofi_agentic_skill # Reload config diff --git a/dotfiles/config/hypr/hyprland.lua b/dotfiles/config/hypr/hyprland.lua new file mode 100644 index 00000000..42d74dc3 --- /dev/null +++ b/dotfiles/config/hypr/hyprland.lua @@ -0,0 +1,1380 @@ +local main_mod = "SUPER" +local mod_alt = "SUPER + ALT" +local hyper = "SUPER + CTRL + ALT" + +local terminal = "ghostty --gtk-single-instance=false" +local menu = "rofi -show drun -show-icons" +local run_menu = "rofi -show run" + +local max_workspace = 9 +local scratchpad_top_margin = 60 +local columns_layout = "nStack" +local monocle_layout = "monocle" +local current_layout = columns_layout +local workspace_layouts = {} +local minimized_windows = {} +local window_picker_mode = nil +local window_picker_candidates = {} +local stack_update_timer = nil +local monocle_notice = nil +local scratchpad_pending = {} +local monitor_workspace_history = {} + +local scratchpads = { + 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", + class = "Element", + }, + slack = { + command = "slack", + class = "Slack", + }, + transmission = { + command = "transmission-gtk", + class = "transmission-gtk", + }, + dropdown = { + command = "ghostty --config-file=/home/imalison/.config/ghostty/dropdown", + class = "com.mitchellh.ghostty.dropdown", + dropdown = true, + }, + gmail = { + command = "google-chrome-stable --new-window https://mail.google.com/mail/u/0/#inbox", + class = "google-chrome", + title = "Gmail", + }, + messages = { + command = "google-chrome-stable --new-window https://messages.google.com/web/conversations", + class = "google-chrome", + title = "Messages", + }, +} + +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 apply_nstack_config() + if verify_config then + return + end + + hl.config({ + plugin = { + nstack = { + layout = { + orientation = "left", + new_on_top = false, + 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 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 scratchpad_window_matches(window, def) + return window + and lower_contains(window.class, 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 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) +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 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 + +local function update_nstack_count() + if current_layout ~= columns_layout then + return + end + + local workspace = hl.get_active_workspace() + local count = tiled_window_count(workspace) + if count == 0 then + return + end + hl.dsp.layout("setstackcount " .. tostring(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 = "info", + color = "rgba(edb443ff)", + font_size = 13, + }) + monocle_notice:pause() + end +end + +local function set_layout(layout) + workspace_layouts[workspace_key()] = layout + current_layout = layout + hl.config({ general = { layout = layout } }) + write_layout_state() + + if layout == columns_layout then + dismiss_monocle_notice() + schedule_nstack_count_update() + else + update_monocle_notice() + end +end + +local function sync_layout_for_active_workspace() + current_layout = current_workspace_layout() + hl.config({ general = { layout = current_layout } }) + write_layout_state() + + if current_layout == columns_layout then + dismiss_monocle_notice() + schedule_nstack_count_update() + else + update_monocle_notice() + end +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 monocle_next() + if current_layout == monocle_layout then + hl.dsp.layout("cyclenext")() + update_monocle_notice() + else + hl.dsp.window.cycle_next({ next = true, tiled = true, floating = false })() + end +end + +local function monocle_prev() + if current_layout == monocle_layout then + hl.dsp.layout("cycleprev")() + update_monocle_notice() + else + hl.dsp.window.cycle_next({ next = false, tiled = true, floating = false })() + end +end + +local function focus_workspace(workspace_id) + hl.dsp.focus({ workspace = tostring(workspace_id), on_current_monitor = true })() +end + +local function monitor_key(monitor) + if not monitor then + return "unknown" + end + return tostring(monitor.name or monitor.id or "unknown") +end + +local function remember_workspace_for_monitor(workspace) + workspace = workspace or active_workspace() + if not workspace or not workspace.id or workspace.id < 1 then + return + end + + local key = monitor_key(workspace.monitor or hl.get_active_monitor()) + local history = monitor_workspace_history[key] or {} + if history.current ~= workspace.id then + history.previous = history.current + history.current = workspace.id + end + monitor_workspace_history[key] = history +end + +local function focus_previous_workspace_for_monitor() + local key = monitor_key(hl.get_active_monitor()) + local history = monitor_workspace_history[key] + if history and history.previous then + focus_workspace(history.previous) + else + hl.dsp.focus({ workspace = "previous_per_monitor" })() + end +end + +local function move_window_to_workspace(workspace_id, follow, window) + hl.dsp.window.move({ workspace = tostring(workspace_id), follow = follow, window = window })() + if follow then + focus_workspace(workspace_id) + end +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 = "info", + color = "rgba(edb443ff)", + font_size = 13, + }) + 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() + hl.dsp.window.move({ monitor = direction, follow = follow, window = window })() + + if not follow and original_monitor then + 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 + + hl.dsp.focus({ monitor = target_monitor })() + focus_workspace(workspace_id) + 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 = {} + for _, window in ipairs(minimized_windows) do + if window and window ~= target then + remaining[#remaining + 1] = window + end + end + minimized_windows = remaining +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 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 width + local height + local x + local y + if def.dropdown then + width = monitor.width + height = math.floor(monitor.height * 0.5) + x = monitor.x + y = monitor.y + scratchpad_top_margin + else + width = math.floor(monitor.width * 0.95) + height = math.min(math.floor(monitor.height * 0.95), monitor.height - scratchpad_top_margin) + x = monitor.x + math.floor((monitor.width - width) / 2) + y = monitor.y + scratchpad_top_margin + end + + hl.dsp.window.float({ action = "enable", window = window })() + hl.dsp.window.tag({ tag = "+scratchpad", window = window })() + hl.dsp.window.tag({ tag = "+scratchpad-" .. name, window = window })() + hl.dsp.window.resize({ x = width, y = height, relative = false, window = window })() + hl.dsp.window.move({ x = x, y = y, relative = false, window = window })() + if def.dropdown then + hl.dsp.window.set_prop({ prop = "border_size", value = "0", window = window })() + hl.dsp.window.set_prop({ prop = "no_shadow", value = "1", window = window })() + end +end + +local function schedule_scratchpad_geometry(name, window, target_monitor) + hl.timer(function() + apply_scratchpad_geometry(name, window, target_monitor) + end, { timeout = 50, type = "oneshot" }) +end + +local function hide_scratchpad_window(name, window) + remove_minimized_window(window) + move_window_to_workspace(scratchpad_workspace(name), false, window) +end + +local function show_scratchpad_window(name, window, workspace, target_monitor) + workspace = workspace or active_workspace() + if not workspace then + return + end + + remove_minimized_window(window) + move_window_to_workspace(workspace.id, false, window) + hl.dsp.focus({ window = 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 window.workspace == workspace +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 target_monitor = scratchpad_pending[name] + scratchpad_pending[name] = nil + show_scratchpad_window(name, window, nil, target_monitor) + elseif scratchpad_is_visible(window) then + schedule_scratchpad_geometry(name, window, hl.get_active_monitor()) + end + end + end +end + +local function current_minimized_windows() + local windows = {} + for _, window in ipairs(minimized_windows) do + if window and window.address 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 = {} + hl.dsp.submap("reset")() + + if not window then + return + end + + if mode == "go" then + hl.dsp.focus({ window = window })() + return + end + + local workspace = active_workspace() + if mode == "bring" and workspace then + move_window_to_workspace(workspace.id, false, window) + hl.dsp.focus({ window = window })() + return + end + + if mode == "minimized" and workspace then + remove_minimized_window(window) + restore_minimized_window(window, workspace) + hl.dsp.focus({ window = window })() + return + end + + if mode == "replace" then + local focused = hl.get_active_window() + if focused and focused ~= window then + hl.dsp.window.swap({ target = window, window = focused })() + hl.dsp.focus({ window = 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 = "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 = "info", + color = "rgba(edb443ff)", + font_size = 11, + }) + 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 = "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 + 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 + hl.dsp.focus({ window = 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 = "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), + "Address: " .. tostring(window.address or ""), + "PID: " .. tostring(window.pid or ""), + } + + hl.notification.create({ + text = table.concat(lines, "\n"), + duration = 5000, + icon = "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 + hl.dsp.focus({ window = 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 + + minimized_windows[#minimized_windows + 1] = window + move_window_to_workspace("special:minimized", false, window) +end + +local function restore_last_minimized() + local workspace = active_workspace() + if not workspace then + return + end + + while #minimized_windows > 0 do + local window = table.remove(minimized_windows) + if window and window.address then + restore_minimized_window(window, workspace) + hl.dsp.focus({ window = window })() + return + end + end +end + +local function restore_all_minimized() + local workspace = active_workspace() + if not workspace then + return + end + + 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 + minimized_windows[#minimized_windows + 1] = window + move_window_to_workspace("special:minimized", 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 + + local remaining = {} + for _, window in ipairs(minimized_windows) do + if window and window.class == focused.class 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 + scratchpad_pending[name] = hl.get_active_monitor() + 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 + 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 + +hl.plugin.load("/run/current-system/sw/lib/libhyprNStack.so") + +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 = 0, + col = { + active_border = { colors = { "rgba(edb443ee)", "rgba(33ccffee)" }, angle = 45 }, + inactive_border = "rgba(595959aa)", + }, + 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 = { + col = { + border_active = "rgba(edb443ff)", + border_inactive = "rgba(091f2eff)", + }, + groupbar = { + enabled = true, + font_size = 12, + height = 22, + col = { + active = "rgba(edb443ff)", + inactive = "rgba(091f2eff)", + }, + text_color = "rgba(091f2eff)", + }, + }, + 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 } } }) + +hl.animation({ leaf = "windows", enabled = true, speed = 6, bezier = "overshoot", style = "gnomed" }) +hl.animation({ leaf = "windowsIn", enabled = true, speed = 6, bezier = "overshoot", style = "gnomed" }) +hl.animation({ leaf = "windowsOut", enabled = true, speed = 5, bezier = "smoothInOut", style = "gnomed" }) +hl.animation({ leaf = "windowsMove", enabled = true, speed = 6, bezier = "smoothOut" }) +hl.animation({ leaf = "border", enabled = false }) +hl.animation({ leaf = "borderangle", enabled = false }) +hl.animation({ leaf = "fade", enabled = true, speed = 5, bezier = "smoothOut" }) +hl.animation({ leaf = "workspaces", enabled = true, speed = 6, bezier = "smoothOut", style = "slidefade 15%" }) +hl.animation({ leaf = "specialWorkspace", enabled = true, speed = 6, bezier = "smoothOut", style = "slidevert" }) + +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 = "^(.*[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, + }) +end + +apply_rules() + +bind(main_mod .. " + P", exec(menu)) +bind(main_mod .. " + SHIFT + P", exec(run_menu)) +bind(main_mod .. " + SHIFT + Return", exec(terminal)) +bind(main_mod .. " + Q", hl.dsp.window.close()) +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", function() + enter_window_picker("go") +end) +bind(main_mod .. " + SHIFT + Tab", function() + enter_window_picker("bring") +end) +bind(main_mod .. " + G", function() + enter_window_picker("go") +end) +bind(main_mod .. " + B", function() + enter_window_picker("bring") +end) +bind(main_mod .. " + SHIFT + B", function() + enter_window_picker("replace") +end) + +bind(main_mod .. " + W", hl.dsp.focus({ direction = "up" })) +bind(main_mod .. " + S", hl.dsp.focus({ direction = "down" })) +bind(main_mod .. " + A", hl.dsp.focus({ direction = "left" })) +bind(main_mod .. " + D", hl.dsp.focus({ direction = "right" })) + +bind(main_mod .. " + SHIFT + W", hl.dsp.window.swap({ direction = "up" })) +bind(main_mod .. " + SHIFT + S", hl.dsp.window.swap({ direction = "down" })) +bind(main_mod .. " + SHIFT + A", hl.dsp.window.swap({ direction = "left" })) +bind(main_mod .. " + SHIFT + D", hl.dsp.window.swap({ direction = "right" })) + +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) + 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", toggle_columns_monocle) +bind(main_mod .. " + SHIFT + Space", function() + set_layout(columns_layout) +end) +bind(main_mod .. " + CTRL + Space", function() + set_layout(monocle_layout) +end) +bind(main_mod .. " + bracketright", monocle_next) +bind(main_mod .. " + bracketleft", monocle_prev) +bind(main_mod .. " + F", hl.dsp.window.fullscreen({ mode = "fullscreen" })) +bind(main_mod .. " + SHIFT + F", hl.dsp.window.fullscreen({ mode = "maximized" })) +bind(main_mod .. " + T", hl.dsp.window.float()) +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", hl.dsp.window.move({ workspace = "special:NSP" })) +bind(main_mod .. " + SHIFT + X", hl.dsp.workspace.toggle_special("NSP")) +bind(mod_alt .. " + E", function() + toggle_scratchpad("element") +end) +bind(mod_alt .. " + G", function() + toggle_scratchpad("gmail") +end) +bind(mod_alt .. " + H", function() + toggle_scratchpad("htop") +end) +bind(mod_alt .. " + M", function() + toggle_scratchpad("messages") +end) +bind(mod_alt .. " + K", function() + toggle_scratchpad("slack") +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 .. " + C", function() + raise_or_spawn("google-chrome", "google-chrome-stable") +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() + hl.dsp.window.move({ workspace = workspace, follow = false })() + hl.dsp.focus({ workspace = workspace, on_current_monitor = true })() + end) +end + +bind(main_mod .. " + backslash", focus_previous_workspace_for_monitor) +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("shell_command.sh")) +bind(hyper .. " + X", exec("rofi_command.sh")) +bind(hyper .. " + SHIFT + L", exec("hyprlock")) +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", exec("toggle_taffybar")) +bind(hyper .. " + I", exec("rofi_select_input.hs")) +bind(hyper .. " + backslash", exec("/home/imalison/dotfiles/dotfiles/lib/functions/mpg341cx_input toggle")) +bind(hyper .. " + O", exec("rofi_paswitch")) +bind(hyper .. " + comma", exec("rofi_wallpaper.sh")) +bind(hyper .. " + Y", exec("rofi_agentic_skill")) +bind(main_mod .. " + R", exec("hyprctl reload")) + +bind(main_mod .. " + mouse:272", hl.dsp.window.drag()) +bind(main_mod .. " + mouse:273", hl.dsp.window.resize()) + +hl.on("hyprland.start", function() + apply_nstack_config() + hl.exec_cmd("sh -lc 'export IMALISON_SESSION_TYPE=wayland; dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY XAUTHORITY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP XDG_SESSION_TYPE IMALISON_SESSION_TYPE; systemctl --user start graphical-session.target 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") + remember_workspace_for_monitor() + write_layout_state() + schedule_nstack_count_update() +end) + +hl.on("config.reloaded", apply_nstack_config) + +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("workspace.active", remember_workspace_for_monitor) + +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/xmonad/xmonad.hs b/dotfiles/config/xmonad/xmonad.hs index 620d59d2..16ed5287 100644 --- a/dotfiles/config/xmonad/xmonad.hs +++ b/dotfiles/config/xmonad/xmonad.hs @@ -1012,11 +1012,11 @@ addKeys conf@XConfig { modMask = modm } = (modm .|. shiftMask) (`windowSwap` True) ++ buildDirectionalBindings (modm .|. controlMask) (followingWindow . (`windowToScreen` True)) ++ + buildDirectionalBindings + (modm .|. controlMask .|. shiftMask) shiftToEmptyOnScreen ++ buildDirectionalBindings hyper (`screenGo` True) ++ buildDirectionalBindings (hyper .|. shiftMask) (followingWindow . (`screenSwap` True)) ++ - buildDirectionalBindings - (hyper .|. controlMask) shiftToEmptyOnScreen ++ -- Specific program spawning bindBringAndRaiseMany @@ -1101,7 +1101,7 @@ addKeys conf@XConfig { modMask = modm } = , ((hyper, xK_space), spawn "skippy-xd") , ((hyper, xK_i), spawn "rofi_select_input.hs") , ((hyper, xK_o), spawn "rofi_paswitch") - , ((hyper, xK_w), spawn "rofi_wallpaper.sh") + , ((hyper, xK_comma), spawn "rofi_wallpaper.sh") , ((hyper, xK_y), spawn "rofi_agentic_skill") , ((modm, xK_e), spawn "emacsclient --eval '(emacs-everywhere)'") diff --git a/nixos/flake.lock b/nixos/flake.lock index 847adbce..fb1051f7 100644 --- a/nixos/flake.lock +++ b/nixos/flake.lock @@ -186,6 +186,29 @@ "type": "github" } }, + "codex-desktop-linux": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1777351752, + "narHash": "sha256-kwdZPCidd9kPYASk6fUPcDfg2uDQ9NzwtYqLlwwzFVk=", + "owner": "ilysenko", + "repo": "codex-desktop-linux", + "rev": "40fd7a8bd6f229e23194881b972fddb2dc42c4c8", + "type": "github" + }, + "original": { + "owner": "ilysenko", + "repo": "codex-desktop-linux", + "type": "github" + } + }, "coqui-tts-streamer": { "inputs": { "flake-utils": [ @@ -335,15 +358,15 @@ "flake-compat_3": { "flake": false, "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "owner": "edolstra", + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { - "owner": "edolstra", + "owner": "NixOS", "repo": "flake-compat", "type": "github" } @@ -353,13 +376,13 @@ "locked": { "lastModified": 1767039857, "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", - "owner": "NixOS", + "owner": "edolstra", "repo": "flake-compat", "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { - "owner": "NixOS", + "owner": "edolstra", "repo": "flake-compat", "type": "github" } @@ -367,11 +390,11 @@ "flake-compat_5": { "flake": false, "locked": { - "lastModified": 1767039857, - "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "owner": "edolstra", "repo": "flake-compat", - "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "type": "github" }, "original": { @@ -502,24 +525,6 @@ "type": "github" } }, - "flake-utils_2": { - "inputs": { - "systems": "systems_3" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "git-blame-rank": { "inputs": { "fenix": "fenix", @@ -685,7 +690,6 @@ "gitignore_3": { "inputs": { "nixpkgs": [ - "imalison-taffybar", "taffybar", "weeder-nix", "pre-commit-hooks", @@ -731,7 +735,7 @@ "hercules-ci-effects_2": { "inputs": { "flake-parts": "flake-parts_5", - "nixpkgs": "nixpkgs_7" + "nixpkgs": "nixpkgs_6" }, "locked": { "lastModified": 1701009247, @@ -789,6 +793,29 @@ "type": "github" } }, + "hyprNStack": { + "inputs": { + "hyprland": [ + "hyprland-lua-config" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1777317717, + "narHash": "sha256-Rj4vx0RvEWtnpnizggWRtrGe092bXiGLLt0WijwYWtI=", + "owner": "colonelpanic8", + "repo": "hyprNStack", + "rev": "94607cd53f2ddac88f6b26261393275e7dd590ef", + "type": "github" + }, + "original": { + "owner": "colonelpanic8", + "repo": "hyprNStack", + "type": "github" + } + }, "hyprcursor": { "inputs": { "hyprlang": [ @@ -1493,7 +1520,9 @@ "nixpkgs": [ "nixpkgs" ], - "taffybar": "taffybar", + "taffybar": [ + "taffybar" + ], "xmonad": [ "xmonad" ] @@ -1574,10 +1603,10 @@ }, "nix": { "inputs": { - "flake-compat": "flake-compat_4", + "flake-compat": "flake-compat_3", "flake-parts": "flake-parts", "git-hooks-nix": "git-hooks-nix", - "nixpkgs": "nixpkgs_5", + "nixpkgs": "nixpkgs_4", "nixpkgs-23-11": "nixpkgs-23-11", "nixpkgs-regression": "nixpkgs-regression" }, @@ -1634,7 +1663,7 @@ }, "nixos-wsl": { "inputs": { - "flake-compat": "flake-compat_5", + "flake-compat": "flake-compat_4", "nixpkgs": [ "nixpkgs" ] @@ -1768,22 +1797,6 @@ } }, "nixpkgs_4": { - "locked": { - "lastModified": 1776877367, - "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "0726a0ecb6d4e08f6adced58726b95db924cef57", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_5": { "locked": { "lastModified": 1771903837, "narHash": "sha256-jEA8WggGKtMFeNeCKq3NK8cLEjJmG6/RLUElYYbBZ0E=", @@ -1796,7 +1809,7 @@ "url": "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz" } }, - "nixpkgs_6": { + "nixpkgs_5": { "locked": { "lastModified": 1776877367, "narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=", @@ -1812,7 +1825,7 @@ "type": "github" } }, - "nixpkgs_7": { + "nixpkgs_6": { "locked": { "lastModified": 1697723726, "narHash": "sha256-SaTWPkI8a5xSHX/rrKzUe+/uVNy6zCGMXgoeMb7T9rg=", @@ -1828,7 +1841,7 @@ "type": "github" } }, - "nixpkgs_8": { + "nixpkgs_7": { "locked": { "lastModified": 1703255338, "narHash": "sha256-Z6wfYJQKmDN9xciTwU3cOiOk+NElxdZwy/FiHctCzjU=", @@ -1848,7 +1861,7 @@ "inputs": { "flake-parts": "flake-parts_4", "hercules-ci-effects": "hercules-ci-effects_2", - "nixpkgs": "nixpkgs_8", + "nixpkgs": "nixpkgs_7", "osx-kvm": "osx-kvm" }, "locked": { @@ -2034,10 +2047,9 @@ }, "pre-commit-hooks_3": { "inputs": { - "flake-compat": "flake-compat_3", + "flake-compat": "flake-compat_5", "gitignore": "gitignore_3", "nixpkgs": [ - "imalison-taffybar", "nixpkgs" ] }, @@ -2108,6 +2120,7 @@ "caelestia-shell": "caelestia-shell", "claude-code-nix": "claude-code-nix", "codex-cli-nix": "codex-cli-nix", + "codex-desktop-linux": "codex-desktop-linux", "coqui-tts-streamer": "coqui-tts-streamer", "flake-utils": "flake-utils", "git-blame-rank": "git-blame-rank", @@ -2115,6 +2128,7 @@ "git-sync-rs": "git-sync-rs", "home-manager": "home-manager", "hy3": "hy3", + "hyprNStack": "hyprNStack", "hyprland": "hyprland", "hyprland-lua-config": "hyprland-lua-config", "hyprland-plugins": "hyprland-plugins", @@ -2126,15 +2140,16 @@ "nixified-ai": "nixified-ai", "nixos-hardware": "nixos-hardware", "nixos-wsl": "nixos-wsl", - "nixpkgs": "nixpkgs_6", + "nixpkgs": "nixpkgs_5", "nixtheplanet": "nixtheplanet", "notifications-tray-icon": "notifications-tray-icon", "org-agenda-api": "org-agenda-api", "railbird-secrets": "railbird-secrets", - "systems": "systems_4", + "systems": "systems_3", + "taffybar": "taffybar", "vscode-server": "vscode-server", - "xmonad": "xmonad_2", - "xmonad-contrib": "xmonad-contrib_2" + "xmonad": "xmonad", + "xmonad-contrib": "xmonad-contrib" } }, "rust-analyzer-src": { @@ -2236,41 +2251,34 @@ "type": "github" } }, - "systems_4": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, "taffybar": { "inputs": { - "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_4", + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ], "weeder-nix": "weeder-nix", - "xmonad": "xmonad", - "xmonad-contrib": "xmonad-contrib" + "xmonad": [ + "xmonad" + ], + "xmonad-contrib": [ + "xmonad-contrib" + ] }, "locked": { - "lastModified": 1777396416, - "narHash": "sha256-uuNyU7wO1pSBN7zxC3sCJ4uPmnDgpaW+0lQvJvYEoK8=", - "ref": "refs/heads/master", - "rev": "ba979b03486c9f27bd67da9cf85152fa49df09ec", - "revCount": 2291, - "type": "git", - "url": "file:///home/imalison/dotfiles/dotfiles/config/taffybar/taffybar" + "lastModified": 1777401169, + "narHash": "sha256-bciN/qFjXYm8ZIKXSc/OssUsLt9GoNs/cU9xT/pw7QY=", + "owner": "taffybar", + "repo": "taffybar", + "rev": "59e3c75990156dcd4353ad9fad5823303e751f0f", + "type": "github" }, "original": { - "type": "git", - "url": "file:///home/imalison/dotfiles/dotfiles/config/taffybar/taffybar" + "owner": "taffybar", + "repo": "taffybar", + "type": "github" } }, "vscode-server": { @@ -2299,7 +2307,6 @@ "weeder-nix": { "inputs": { "nixpkgs": [ - "imalison-taffybar", "taffybar", "nixpkgs" ], @@ -2402,7 +2409,20 @@ } }, "xmonad": { - "flake": false, + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "git-ignore-nix": [ + "git-ignore-nix" + ], + "nixpkgs": [ + "nixpkgs" + ], + "unstable": [ + "nixpkgs" + ] + }, "locked": { "lastModified": 1776502138, "narHash": "sha256-mSOpNU1iJvfFh5uwayA6aPxneFMduNW1kG1gV2tGE+c=", @@ -2413,29 +2433,11 @@ }, "original": { "owner": "xmonad", - "ref": "master", "repo": "xmonad", "type": "github" } }, "xmonad-contrib": { - "flake": false, - "locked": { - "lastModified": 1769258911, - "narHash": "sha256-YGEKXs4UmS5QOIELJTdCiMzTktuue+Bd3yFoIKSHuBU=", - "owner": "xmonad", - "repo": "xmonad-contrib", - "rev": "803bc3d12bdcc512ec06856c4f119d37de1ba338", - "type": "github" - }, - "original": { - "owner": "xmonad", - "ref": "master", - "repo": "xmonad-contrib", - "type": "github" - } - }, - "xmonad-contrib_2": { "inputs": { "flake-utils": [ "flake-utils" @@ -2464,35 +2466,6 @@ "repo": "xmonad-contrib", "type": "github" } - }, - "xmonad_2": { - "inputs": { - "flake-utils": [ - "flake-utils" - ], - "git-ignore-nix": [ - "git-ignore-nix" - ], - "nixpkgs": [ - "nixpkgs" - ], - "unstable": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1776502138, - "narHash": "sha256-mSOpNU1iJvfFh5uwayA6aPxneFMduNW1kG1gV2tGE+c=", - "owner": "xmonad", - "repo": "xmonad", - "rev": "a618fb32662e44eb5d8276a3dc1925b0233e638b", - "type": "github" - }, - "original": { - "owner": "xmonad", - "repo": "xmonad", - "type": "github" - } } }, "root": "root", diff --git a/nixos/flake.nix b/nixos/flake.nix index 8afd8c2f..e444d49d 100644 --- a/nixos/flake.nix +++ b/nixos/flake.nix @@ -101,6 +101,14 @@ url = "git+https://github.com/hyprwm/Hyprland?submodules=1&ref=refs/pull/13817/head"; }; + hyprNStack = { + url = "github:colonelpanic8/hyprNStack"; + inputs = { + hyprland.follows = "hyprland-lua-config"; + nixpkgs.follows = "nixpkgs"; + }; + }; + hy3 = { url = "github:outfoxxed/hy3?ref=hl0.53.0"; inputs.hyprland.follows = "hyprland"; @@ -151,7 +159,7 @@ taffybar = { # Use a remote, lockfile-pinned input so rebuilds are reproducible across # machines. For local development, use `nixos-rebuild --override-input taffybar path:...`. - url = "github:taffybar/taffybar?ref=codex/fix-gdk-backend-strut-detection"; + url = "github:taffybar/taffybar"; inputs = { nixpkgs.follows = "nixpkgs"; flake-utils.follows = "flake-utils"; @@ -467,6 +475,7 @@ } // flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; + lib = pkgs.lib; # Get short revs for tagging orgApiRev = builtins.substring 0 7 (org-agenda-api.rev or "unknown"); @@ -487,6 +496,147 @@ packages = { colonelpanic-org-agenda-api = containerLib.containers.colonelpanic; kat-org-agenda-api = containerLib.containers.kat; + } // lib.optionalAttrs pkgs.stdenv.isLinux { + hyprNStack = inputs.hyprNStack.packages.${system}.hyprNStack; + }; + + checks = lib.optionalAttrs pkgs.stdenv.isLinux { + hyprNStack = inputs.hyprNStack.packages.${system}.hyprNStack; + hyprland-lua-config-syntax = pkgs.runCommand "hyprland-lua-config-syntax" { + nativeBuildInputs = [ pkgs.lua5_4 ]; + } '' + luac -p ${../dotfiles/config/hypr/hyprland.lua} + if grep -n 'hyprctl' ${../dotfiles/config/hypr/hyprland.lua} | grep -v 'hyprctl reload'; then + echo "hyprland.lua should not shell out to hyprctl for window/workspace manipulation" >&2 + exit 1 + fi + lua <<'LUA' + local callbacks = {} + + local function noop() end + + local function dispatcher_proxy() + local proxy = {} + return setmetatable(proxy, { + __index = function() + return dispatcher_proxy() + end, + __call = function() + return noop + end, + }) + end + + local notification = { + is_alive = function() + return true + end, + set_text = noop, + set_timeout = noop, + pause = noop, + resume = noop, + set_paused = noop, + dismiss = noop, + } + + local monitor = { + id = 1, + name = "stub-monitor", + focused = true, + } + + local workspace = { + id = 1, + name = "1", + windows = 0, + special = false, + monitor = monitor, + } + + monitor.active_workspace = workspace + + hl = { + animation = noop, + bind = noop, + config = noop, + curve = noop, + env = noop, + exec_cmd = noop, + define_submap = function(_, reset_or_callback, callback) + local cb = type(reset_or_callback) == "function" and reset_or_callback or callback + if cb then + cb() + end + end, + monitor = noop, + workspace_rule = noop, + window_rule = noop, + dsp = dispatcher_proxy(), + notification = { + create = function() + return notification + end, + }, + plugin = { + load = noop, + }, + get_active_workspace = function() + return workspace + end, + get_active_monitor = function() + return monitor + end, + get_active_window = function() + return nil + end, + get_monitor = function() + return monitor + end, + get_workspace = function(id) + if tostring(id) == "1" then + return workspace + end + return nil + end, + get_windows = function() + return {} + end, + get_workspace_windows = function() + return {} + end, + on = function(_, callback) + callbacks[#callbacks + 1] = callback + end, + timer = function(callback) + callback() + return { + set_enabled = noop, + } + end, + } + + dofile("${../dotfiles/config/hypr/hyprland.lua}") + + for _, callback in ipairs(callbacks) do + callback() + end + LUA + touch "$out" + ''; + hyprland-lua-verify-config = let + hyprlandPackage = inputs.hyprland-lua-config.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 \ + --replace-fail /run/current-system/sw/lib/libhyprNStack.so \ + ${hyprNStackPackage}/lib/libhyprNStack.so + export XDG_RUNTIME_DIR="$TMPDIR/runtime" + mkdir -p "$XDG_RUNTIME_DIR" + HYPRLAND_NO_CRASHREPORTER=1 ${pkgs.coreutils}/bin/timeout 20s \ + ${hyprlandPackage}/bin/Hyprland --verify-config --config "$PWD/hyprland.lua" + touch "$out" + ''; }; # Dev shell for org-agenda-api deployment diff --git a/nixos/hyprland.nix b/nixos/hyprland.nix index 2e4aaa9a..be5cb8f8 100644 --- a/nixos/hyprland.nix +++ b/nixos/hyprland.nix @@ -7,6 +7,9 @@ let if cfg.useLuaConfigBranch then inputs.hyprland-lua-config else inputs.hyprland; + luaPluginPackages = lib.optionals cfg.useLuaConfigBranch [ + inputs.hyprNStack.packages.${system}.hyprNStack + ]; hyprexpoPatched = inputs.hyprland-plugins.packages.${system}.hyprexpo.overrideAttrs (old: { patches = (old.patches or [ ]) ++ [ ./patches/hyprexpo-pr-612-workspace-numbers.patch @@ -78,8 +81,8 @@ let ]; }; - programs.hyprscratch = { - enable = true; + programs.hyprscratch = { + enable = !cfg.useLuaConfigBranch; settings = { daemon_options = "clean"; global_options = ""; @@ -157,7 +160,7 @@ let # For scripts jq - ] ++ lib.optionals enableExternalPluginPackages [ + ] ++ luaPluginPackages ++ lib.optionals enableExternalPluginPackages [ # External plugin packages are pinned to the stable 0.53 stack. # PR 13817's Hyprland branch builds, but hy3 / hyprexpo do not yet build # against it, so keep them out of the experimental Lua branch for now. @@ -173,11 +176,10 @@ enabledModule // { default = false; description = '' Use the experimental Hyprland PR 13817 Lua-config branch for the - Hyprland package itself. Third-party plugins are left on the stable - stack and are excluded from the experimental package set because current - `hy3` and `hyprexpo` sources do not build against PR 13817 yet. The - existing `hyprland.conf` remains active until a sibling `hyprland.lua` - file is added. + Hyprland package itself. The experimental package set excludes hy3 and + hyprexpo, and includes the Lua-branch build of hyprNStack instead. When + a sibling `hyprland.lua` is present, the Lua config manager picks it + before `hyprland.conf`. ''; }; }; diff --git a/nixos/machines/ryzen-shine.nix b/nixos/machines/ryzen-shine.nix index 240d860a..6974d51f 100644 --- a/nixos/machines/ryzen-shine.nix +++ b/nixos/machines/ryzen-shine.nix @@ -9,6 +9,7 @@ features.full.enable = true; myModules.kubelet.enable = false; myModules.nvidia.enable = true; + myModules.hyprland.useLuaConfigBranch = true; # Needed for now because monitors have different refresh rates myModules.xmonad.picom.vSync.enable = false; myModules.cache-server = {