dot
vizie's NixOS desktop: Hyprland, CachyOS kernel and gaming tweaks
| 1 | -- Generated by hyprlang2lua. Review TODOs before reloading Hyprland. |
| 2 | |
| 3 | local mod = "SUPER" |
| 4 | |
| 5 | --GENERAL# |
| 6 | |
| 7 | --Monitor settings |
| 8 | hl.monitor({ |
| 9 | output = "HDMI-A-2", |
| 10 | mode = "2560x1440@280", |
| 11 | position = "0x0", |
| 12 | scale = 1, |
| 13 | vrr = 0, |
| 14 | }) |
| 15 | |
| 16 | hl.monitor({ |
| 17 | output = "DP-1", |
| 18 | mode = "2560x1440@144", |
| 19 | position = "2560x-900", |
| 20 | scale = 1, |
| 21 | transform = 1, |
| 22 | vrr = 0, |
| 23 | }) |
| 24 | |
| 25 | --monitor=DP-1,disable |
| 26 | |
| 27 | -- WS on HDMI-A-2 |
| 28 | hl.workspace_rule({ |
| 29 | workspace = "1", |
| 30 | monitor = "HDMI-A-2", |
| 31 | persistent = true, |
| 32 | }) |
| 33 | |
| 34 | hl.workspace_rule({ |
| 35 | workspace = "2", |
| 36 | monitor = "HDMI-A-2", |
| 37 | persistent = true, |
| 38 | }) |
| 39 | |
| 40 | -- WS on DP-1 |
| 41 | hl.workspace_rule({ |
| 42 | workspace = "3", |
| 43 | monitor = "DP-1", |
| 44 | persistent = true, |
| 45 | }) |
| 46 | |
| 47 | hl.workspace_rule({ |
| 48 | workspace = "4", |
| 49 | monitor = "DP-1", |
| 50 | persistent = true, |
| 51 | }) |
| 52 | |
| 53 | hl.workspace_rule({ |
| 54 | workspace = "5", |
| 55 | monitor = "DP-1", |
| 56 | persistent = true, |
| 57 | }) |
| 58 | |
| 59 | hl.window_rule({ |
| 60 | name = "windowrule-1", |
| 61 | match = { |
| 62 | title = "^RuneLite.*", |
| 63 | }, |
| 64 | float = true, |
| 65 | }) |
| 66 | |
| 67 | hl.window_rule({ |
| 68 | name = "windowrule-2", |
| 69 | match = { |
| 70 | class = "net-runelite-client-RuneLite", |
| 71 | title = "win.*", |
| 72 | }, |
| 73 | no_focus = true, |
| 74 | }) |
| 75 | |
| 76 | hl.window_rule({ |
| 77 | name = "windowrule-3", |
| 78 | match = { |
| 79 | class = ".*", |
| 80 | }, |
| 81 | immediate = true, |
| 82 | }) |
| 83 | |
| 84 | -- Battle.net (Proton via Faugus). The class is shared by every umu game without |
| 85 | -- its own ID, so match on the title too. |
| 86 | hl.window_rule({ |
| 87 | name = "battlenet-float", |
| 88 | match = { |
| 89 | class = "^steam_app_default$", |
| 90 | title = "^Battle\\.net.*", |
| 91 | }, |
| 92 | float = true, |
| 93 | }) |
| 94 | |
| 95 | -- xembedsniproxy (pkgs/xembedsniproxy) puts Wine/Proton tray icons such as |
| 96 | -- Battle.net into Waybar's tray. It keeps a small helper window mapped for the |
| 97 | -- icons it hosts, which Hyprland would otherwise show as a floating window. |
| 98 | hl.window_rule({ |
| 99 | name = "xembedsniproxy", |
| 100 | match = { |
| 101 | class = "^xembedsniproxy$", |
| 102 | }, |
| 103 | opacity = "0.0 override 0.0 override", |
| 104 | border_size = 0, |
| 105 | no_blur = true, |
| 106 | no_shadow = true, |
| 107 | no_anim = true, |
| 108 | no_focus = true, |
| 109 | no_initial_focus = true, |
| 110 | }) |
| 111 | |
| 112 | -- Dolphin transparency now comes from Kvantum's own translucent_windows |
| 113 | -- (see kvantum/RiceDark.kvconfig) rather than a forced window opacity here: |
| 114 | -- Kvantum punches real per-pixel alpha into the background only, so text and |
| 115 | -- icons stay fully opaque and legible, same mechanism as kitty (which also |
| 116 | -- has no windowrule here) -- decoration.blur is already global+enabled above |
| 117 | -- with ignore_opacity defaulting true, so it blurs through automatically. |
| 118 | -- If Dolphin still looks flat/opaque after applying the Kvantum theme, that |
| 119 | -- rule can come back as a fallback: |
| 120 | -- hl.window_rule({ |
| 121 | -- name = "windowrule-4", |
| 122 | -- match = { class = "^(org.kde.dolphin)$" }, |
| 123 | -- opacity = "0.90 0.80", |
| 124 | -- }) |
| 125 | |
| 126 | -- Claude's desktop app creates its window opaque (transparent: false in the |
| 127 | -- Electron bundle) and has no user stylesheet, so unlike kitty and Dolphin it |
| 128 | -- can't put alpha in the background only. A forced window opacity is the |
| 129 | -- closest it gets: decoration.blur frosts what shows through, at the cost of |
| 130 | -- the text being translucent too. |
| 131 | hl.window_rule({ |
| 132 | name = "claude-frosted", |
| 133 | match = { class = "^com\\.anthropic\\.Claude$" }, |
| 134 | opacity = "0.80 0.80", |
| 135 | }) |
| 136 | |
| 137 | --windowrulev2 = maxsize 1803 1153,title:^(RuneLite)$ |
| 138 | --windowrulev2 = maxsize 1803 1153,title:^(RuneLite)$ |
| 139 | |
| 140 | hl.layer_rule({ |
| 141 | name = "layerrule-1", |
| 142 | match = { |
| 143 | namespace = "waybar", |
| 144 | }, |
| 145 | blur = true, |
| 146 | ignore_alpha = 0, |
| 147 | blur_popups = true, |
| 148 | }) |
| 149 | |
| 150 | hl.layer_rule({ |
| 151 | name = "layerrule-2", |
| 152 | match = { |
| 153 | namespace = "launcher", -- fuzzel |
| 154 | }, |
| 155 | ignore_alpha = 0, |
| 156 | blur = true, |
| 157 | }) |
| 158 | |
| 159 | hl.layer_rule({ |
| 160 | name = "layerrule-3", |
| 161 | match = { |
| 162 | namespace = "notifications", |
| 163 | }, |
| 164 | ignore_alpha = 0, |
| 165 | blur = true, |
| 166 | }) |
| 167 | |
| 168 | hl.layer_rule({ |
| 169 | name = "layerrule-4", |
| 170 | match = { |
| 171 | namespace = "eww-control-center", |
| 172 | }, |
| 173 | ignore_alpha = 0, |
| 174 | blur = true, |
| 175 | }) |
| 176 | |
| 177 | hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd("imsh-shot area -Cufa@~/.config/keys/scrnis -s1")) |
| 178 | hl.bind(mod .. " + SHIFT + F", hl.dsp.exec_cmd("imsh-shot screen -Cua@~/.config/keys/scrnis")) |
| 179 | hl.bind(mod .. " + SHIFT + R", hl.dsp.exec_cmd("imsh-cast -Cua@~/.config/keys/scrnis")) |
| 180 | hl.bind(mod .. " + Return", hl.dsp.exec_cmd("~/.config/fuzzel/launch")) |
| 181 | hl.bind(mod .. " + T", hl.dsp.exec_cmd("kitty")) |
| 182 | hl.bind(mod .. " + W", hl.dsp.exec_cmd("firefox")) |
| 183 | hl.bind(mod .. " + Q", hl.dsp.window.close()) |
| 184 | hl.bind(mod .. " + F", hl.dsp.window.float({ action = "toggle" })) |
| 185 | hl.bind(mod .. " + G", hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" })) |
| 186 | hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "m+1" })) |
| 187 | hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "m-1" })) |
| 188 | hl.bind(mod .. " + mouse:272", hl.dsp.window.drag()) |
| 189 | hl.bind(mod .. " + mouse:273", hl.dsp.window.resize()) |
| 190 | hl.bind(mod .. " + Delete", hl.dsp.exit()) |
| 191 | |
| 192 | hl.env("LIBVA_DRIVER_NAME", "nvidia") |
| 193 | hl.env("NVD_BACKEND", "direct") -- libva-nvidia-driver backend (Firefox hardware video decoding) |
| 194 | hl.env("MOZ_DISABLE_RDD_SANDBOX", "1") -- let Firefox's video decoder process reach the NVIDIA driver |
| 195 | hl.env("XDG_SESSION_TYPE", "wayland") |
| 196 | hl.env("GBM_BACKEND", "nvidia-drm") |
| 197 | hl.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia") |
| 198 | hl.env("XCURSOR_THEME", "BreezeX-Black") |
| 199 | hl.env("XCURSOR_SIZE", "28") |
| 200 | hl.env("QT_QPA_PLATFORMTHEME", "qt6ct") |
| 201 | |
| 202 | hl.config({ |
| 203 | general = { |
| 204 | border_size = 1, |
| 205 | gaps_in = 5, |
| 206 | gaps_out = 17, |
| 207 | layout = "dwindle", |
| 208 | col = { |
| 209 | active_border = "rgba(00000000)", |
| 210 | inactive_border = "rgba(00000000)", |
| 211 | }, |
| 212 | allow_tearing = true, |
| 213 | }, |
| 214 | decoration = { |
| 215 | rounding = 10, |
| 216 | -- No visible borders; the focused window is the one that isn't dimmed. |
| 217 | dim_inactive = true, |
| 218 | dim_strength = 0.15, |
| 219 | blur = { |
| 220 | enabled = true, |
| 221 | size = 9, |
| 222 | passes = 3, |
| 223 | new_optimizations = true, |
| 224 | popups = true, -- blur tooltips/context menus (Kvantum's popup_blurring is on for this) |
| 225 | }, |
| 226 | shadow = { |
| 227 | enabled = false, |
| 228 | render_power = 3, |
| 229 | range = 4, |
| 230 | color = "rgba(1a1a1aee)", |
| 231 | }, |
| 232 | }, |
| 233 | input = { |
| 234 | kb_layout = "no", |
| 235 | follow_mouse = 1, |
| 236 | sensitivity = 0, |
| 237 | accel_profile = "flat", |
| 238 | }, |
| 239 | --MISC# |
| 240 | misc = { |
| 241 | disable_hyprland_logo = true, |
| 242 | disable_splash_rendering = true, |
| 243 | -- Wallpaper's average colour instead of black, so the moment between the |
| 244 | -- login screen and hyprpaper drawing the wallpaper doesn't flash black. |
| 245 | background_color = "rgb(121d1a)", |
| 246 | }, |
| 247 | ecosystem = { |
| 248 | no_donation_nag = true, |
| 249 | }, |
| 250 | --BINDS# |
| 251 | binds = { |
| 252 | scroll_event_delay = 10, |
| 253 | }, |
| 254 | --EXEC# |
| 255 | --ENVIROMENT# |
| 256 | --CURSOR# |
| 257 | cursor = { |
| 258 | -- Software cursor, for tearing. Hyprland (0.56) refuses to tear while a |
| 259 | -- hardware cursor is visible, because the kernel can't update the cursor |
| 260 | -- plane during a tearing flip. So with a hardware cursor a game only tears |
| 261 | -- while it hides the pointer (e.g. mouselook) and is vsynced otherwise, and |
| 262 | -- switching back and forth left WoW's cursor shape stale. With a software |
| 263 | -- cursor the game tears all the time; the price is that frames are |
| 264 | -- composited (one extra copy) while the pointer is visible. |
| 265 | no_hardware_cursors = true, |
| 266 | }, |
| 267 | render = { |
| 268 | -- Fullscreen games (e.g. WoW) scan out straight to the monitor instead of |
| 269 | -- being copied by the compositor. With the software cursor above this only |
| 270 | -- applies while the game hides the pointer. |
| 271 | -- 1 = always; XWayland games don't set a content type, so 2 (auto) won't trigger. |
| 272 | direct_scanout = 1, |
| 273 | }, |
| 274 | }) |
| 275 | |
| 276 | hl.on("hyprland.start", function() |
| 277 | -- Wallpaper, bar, notifications, the control centre, tray helpers, the |
| 278 | -- clipboard bridge and the polkit agent are systemd user services now |
| 279 | -- (modules/home/services.nix in the dot repo): uwsm starts them with |
| 280 | -- graphical-session.target, restarts them if they crash, and |
| 281 | -- `systemctl --user status <name>` / `journalctl --user -u <name>` show them. |
| 282 | |
| 283 | -- Apps opened at login, in their usual places (scripts/autostart-apps starts |
| 284 | -- them, from autostart-apps.service). These placement rules are switched off |
| 285 | -- again after two minutes, so opening the same apps by hand later behaves like |
| 286 | -- any other window. |
| 287 | local startup_rules = { |
| 288 | hl.window_rule({ |
| 289 | name = "startup-signal", |
| 290 | match = { class = "^signal$" }, |
| 291 | workspace = "3 silent", |
| 292 | }), |
| 293 | hl.window_rule({ |
| 294 | name = "startup-discord", |
| 295 | match = { class = "^discord$" }, |
| 296 | workspace = "3 silent", |
| 297 | }), |
| 298 | hl.window_rule({ |
| 299 | name = "startup-faugus", |
| 300 | match = { class = "^io\\.github\\.Faugus\\.faugus-launcher$" }, |
| 301 | workspace = "1 silent", |
| 302 | float = true, |
| 303 | size = { 430, 610 }, |
| 304 | move = { 1901, 333 }, |
| 305 | }), |
| 306 | } |
| 307 | hl.timer(function() |
| 308 | for _, rule in ipairs(startup_rules) do |
| 309 | rule:set_enabled(false) |
| 310 | end |
| 311 | end, { timeout = 120000, type = "oneshot" }) |
| 312 | |
| 313 | -- WowUp can't start hidden, so the window it opens at login is sent to a hidden |
| 314 | -- workspace and closed again, which leaves it in the tray. That rule must not |
| 315 | -- outlive the one window: it is dropped as soon as the window has opened (or |
| 316 | -- after a minute if none came), so opening WowUp yourself right after login |
| 317 | -- lands on your workspace like any other window. |
| 318 | local wowup_rule = hl.window_rule({ |
| 319 | name = "startup-wowup", |
| 320 | match = { class = "^WowUpCf$" }, |
| 321 | workspace = "special:autostart silent", |
| 322 | }) |
| 323 | local wowup_pending = true |
| 324 | local |
| 325 | wowup_pending = false |
| 326 | wowup_rule:set_enabled(false) |
| 327 | end |
| 328 | hl.on("window.open", function(window) |
| 329 | if wowup_pending and window.class == "WowUpCf" then |
| 330 | wowup_done() |
| 331 | -- closing it from inside the open event is too early and gets ignored |
| 332 | local address = window.address |
| 333 | hl.timer(function() |
| 334 | hl.dispatch(hl.dsp.window.close({ window = "address:" .. address })) |
| 335 | end, { timeout = 300, type = "oneshot" }) |
| 336 | end |
| 337 | end) |
| 338 | hl.timer(wowup_done, { timeout = 60000, type = "oneshot" }) |
| 339 | |
| 340 | -- hl.exec_cmd("gsettings set org.gnome.desktop.interface color-scheme prefer-dark") |
| 341 | -- hl.exec_cmd("gsettings set org.gnome.desktop.interface gtk-theme Adwaita-dark") |
| 342 | end) |
| 343 | |