dot

vizie's NixOS desktop: Hyprland, CachyOS kernel and gaming tweaks

6bafe8f

Home Manager modules for the desktop and apps; Firefox on its own profile

By Gustaf Granström

Hyprland (Lua), hyprlock, hypridle, hyprpaper, Waybar, dunst, kitty,
fuzzel, neovim, Firefox, qt6ct, Kvantum and kdeglobals are now set
through their Home Manager modules (desktop.nix, apps.nix, theming.nix)
instead of linked files, with paths from Nix: the wallpaper, Waybar's
tray icons and logos, the cursor and Qt theme, kitty for fuzzel and
Hyprland. Waybar, dunst, hypridle and hyprpaper use the modules' units,
kept in uwsm's session slice.

What has no module or is code stays linked to the checkout: Hyprland's
startup rules (config/hypr/startup.lua), the scripts, Eww, fuzzel's
launcher, nvim's init.lua, userChrome.css and the Kvantum themes. Configs
apps save themselves (btop, MangoHud, micro, mimeapps.list) stay seeded.

Firefox opened an empty profile after the migration: nixpkgs' Firefox
runs with MOZ_LEGACY_PROFILES and takes the profile marked Default=1,
not CachyOS's [Install] default. programs.firefox now declares the
restored profile by its folder and StoreID.

A one-time activation step removes the old whole-folder links first, so
Home Manager doesn't see the checkout's own files through them as
collisions and move them aside in the repo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

30 files changed +1091 −960

modifiedREADME.md
@@ -24,10 +24,12 @@
2424 apps.nix desktop apps, Claude Cowork, OpenDeck udev, Flatpak
2525 vm.nix the test VM
2626 modules/home/
27- files.nix links config/ into ~/.config (was symlink.sh)
27+ desktop.nix Hyprland, hyprlock, hypridle, hyprpaper, Waybar, dunst
28+ apps.nix kitty, fuzzel, neovim, Firefox
29+ files.nix links the rest of config/ into ~/.config (was symlink.sh)
2830 services.nix desktop background programs as systemd user services
2931 shell.nix zsh (as cachyos-zsh-config), git, gh, ssh
30- theming.nix GTK, cursor
32+ theming.nix GTK, cursor, Qt (qt6ct, Kvantum, kdeglobals)
3133 packages.nix tools the scripts call; imsh-clients, philly
3234 config/ the dotfiles themselves
3335 pkgs/ packages not in nixpkgs, or patched
@@ -44,10 +46,18 @@
4446 Every rebuild is a boot menu entry, so a bad one is undone by picking the one
4547 before it.
4648
47-**Editing configs.** `~/.config/hypr`, `waybar`, `kitty`, `dunst`, `fuzzel`, `nvim`,
48-`qt6ct`, Kvantum, `kdeglobals`, the Philly theme, Firefox's `user.js` and
49-`userChrome.css` and `~/.p10k.zsh` link straight to `config/` in this checkout,
50-like the old `symlink.sh` did, so edits apply without a rebuild. (Set
49+**Editing configs.** Apps with a Home Manager module are set in Nix, in
50+`modules/home/desktop.nix`, `apps.nix` and `theming.nix`: Hyprland, hyprlock,
51+hypridle, hyprpaper, Waybar, dunst, kitty, fuzzel, Firefox's prefs, qt6ct, Kvantum
52+and `kdeglobals`. Paths in them (wallpaper, icons, scripts, the terminal) come
53+from Nix, so change these there and `rebuild`.
54+
55+What has no module, or is code rather than settings, stays in `config/` and is
56+linked straight to this checkout, like the old `symlink.sh` did, so edits apply
57+without a rebuild: Hyprland's startup rules (`hypr/startup.lua`), the scripts
58+under `hypr/scripts` and `waybar/scripts`, the Eww flyouts (`waybar/eww`),
59+fuzzel's launcher and favourites, nvim's `init.lua`, Firefox's `userChrome.css`,
60+the Kvantum themes, the Philly theme and `~/.p10k.zsh`. (Set
5161 `dot.liveConfig = false` to have them copied into the Nix store instead.) If the
5262 checkout is not at `~/git/dot`, set `dot.repoPath`.
5363
@@ -78,8 +88,8 @@
7888 systemctl --user restart waybar
7989 ```
8090
81-`hyprland.lua` keeps only what has to live in the compositor: the startup window
82-rules that place those apps and hide WowUp's window.
91+`config/hypr/startup.lua` keeps only what has to live in the compositor: the
92+startup window rules that place those apps and hide WowUp's window.
8393
8494 ## Gaming
8595
deletedconfig/dunst/dunstrc
@@ -1,49 +1,0 @@
1-[global]
2-monitor = 0
3-follow = mouse
4-alignment = center
5-width = 360
6-offset = 15
7-origin = top-right
8-padding = 12
9-horizontal_padding = 12
10-frame_width = 0
11-markup = full
12-icon_position = off
13-font = "DejaVu Sans Mono 10"
14-stack_duplicates = true
15-hide_duplicate_count = true
16-separator_height = 0
17-fullscreen = pushback
18-corner_radius = 15
19-# Left click does what the notification is for (e.g. opens the chat), then closes
20-# it. Middle click only dismisses it, right click dismisses all.
21-mouse_left_click = do_action, close_current
22-mouse_middle_click = close_current
23-mouse_right_click = close_all
24-progress_bar = true
25-
26-
27-[urgency_low]
28-timeout = 4
29-background = "#25252501"
30-foreground = "#ffffff"
31-frame_color = "#25252501"
32-
33-[urgency_normal]
34-timeout = 8
35-background = "#25252501"
36-foreground = "#ffffff"
37-frame_color = "#25252501"
38-
39-[urgency_critical]
40-timeout = 16
41-background = "#25252501"
42-foreground = "#ffffff"
43-frame_color = "#25252501"
44-
45-[status]
46-category = status
47-set_stack_tag = status
48-history_ignore = yes
49-timeout = 2
deletedconfig/firefox/user.js
@@ -1,28 +1,0 @@
1-// Restore previous session
2-user_pref("browser.startup.page", 3);
3-
4-// Middle mouse scroll
5-user_pref("general.autoScroll", true);
6-
7-// Enable browser toolbox
8-user_pref("devtools.chrome.enabled", true);
9-user_pref("devtools.debugger.remote-enabled", true);
10-
11-user_pref("browser.sessionstore.resume_from_crash", true);
12-user_pref("general.aboutConfig.enable", true);
13-user_pref("middlemouse.paste", false);
14-user_pref("sidebar.animation.enabled", false);
15-// user_pref("image.jxl.enabled", true);
16-user_pref("devtools.cache.disabled", true);
17-user_pref("devtools.netmonitor.persistlog", true);
18-user_pref("devtools.webconsole.persistlog", true);
19-user_pref("devtools.webconsole.timestampMessages", true);
20-
21-user_pref("toolkit.legacyUserProfileCustomizations.stylesheets", true);
22-
23-// Hardware video decoding on NVIDIA via VA-API (libva-nvidia-driver).
24-// Firefox blocks NVIDIA for this by default; needs NVD_BACKEND and
25-// MOZ_DISABLE_RDD_SANDBOX from hyprland.lua as well.
26-user_pref("media.hardware-video-decoding.force-enabled", true);
27-user_pref("media.ffmpeg.vaapi.enabled", true);
28-user_pref("widget.dmabuf.force-enabled", true);
deletedconfig/fuzzel/fuzzel.ini
@@ -1,37 +1,0 @@
1-[main]
2-font=NotoSansM Nerd Font Propo:size=12
3-# Size fonts like GTK apps do (96 DPI) rather than by the monitors' real DPI,
4-# which made everything bigger.
5-dpi-aware=no
6-terminal=kitty
7-# Same icon theme as Qt/KDE (qt6ct, kdeglobals). Without it fuzzel mixes each
8-# app's own icon, in different styles and sizes.
9-icon-theme=Papirus-Dark
10-list-executables-in-path=yes
11-placeholder=Search...
12-line-height=36
13-lines=5
14-width=45
15-horizontal-pad=25
16-vertical-pad=25
17-inner-pad=10
18-
19-# RRGGBBAA. Same near-transparent black as Waybar and the control centre, over
20-# Hyprland's blur (the "launcher" layer rule).
21-[colors]
22-background=0000001a
23-text=f2f2f2cc
24-prompt=f2f2f2cc
25-input=f2f2f2ff
26-placeholder=f2f2f2cc
27-match=f4d69fff
28-selection=0000001a
29-selection-text=f2f2f2ff
30-selection-match=f4d69fff
31-counter=f2f2f2cc
32-
33-# Panel 20, rows inside it 10, like the control centre.
34-[border]
35-width=0
36-radius=20
37-selection-radius=10
@@ -1,6 +1,6 @@
11 #!/usr/bin/env bash
22 # Opens fuzzel with the apps in ./favorites at the top of its list. Hyprland's
3-# mod + Return runs this (hyprland.lua).
3+# mod + Return runs this (modules/home/desktop.nix).
44 #
55 # Fuzzel can't pin apps; it sorts by how often each one was launched, counted in
66 # its cache ($XDG_CACHE_HOME/fuzzel, lines of "name.desktop|count"). So before
@@ -1,4 +1,4 @@
1-/* greetd login screen (gtkgreet -l), styled like hypr/hyprlock.conf in the dotfiles:
1+/* greetd login screen (gtkgreet -l), styled like hyprlock (modules/home/desktop.nix):
22 full-screen layer, Hyprland blurs the wallpaper behind it (layer rule in
33 hyprland.lua), translucent black boxes, #eee text, rounding 10. */
44 * {
deletedconfig/hypr/hypridle.conf
@@ -1,24 +1,0 @@
1-general {
2- lock_cmd=pidof hyprlock || hyprlock
3- before_sleep_cmd=loginctl lock-session
4- after_sleep_cmd=hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
5-}
6-
7-# Lock after 10 minutes idle (respects inhibitors, e.g. fullscreen video).
8-listener {
9- timeout=600
10- on-timeout=loginctl lock-session
11-}
12-
13-listener {
14- timeout=1
15- on-resume=hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
16- on-timeout=true
17- ignore_inhibit=true
18-}
19-
20-listener {
21- timeout=300
22- on-timeout=hyprctl dispatch 'hl.dsp.dpms({ action = "off" })'
23- ignore_inhibit=true
24-}
deletedconfig/hypr/hyprland.lua
@@ -1,343 +1,0 @@
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 function wowup_done()
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-
deletedconfig/hypr/hyprlock.conf
@@ -1,89 +1,0 @@
1-# Lock screen, styled to match the greetd login screen (/etc/greetd/style.css):
2-# same bamboo wallpaper, translucent black boxes, #eee text, rounding 10.
3-
4-$font = NotoSans Nerd Font
5-$text = rgb(238, 238, 238)
6-$box = rgba(0, 0, 0, 0.2)
7-
8-general {
9- hide_cursor = false
10- ignore_empty_input = true
11-}
12-
13-animations {
14- enabled = true
15- bezier = ease, 0.25, 0.1, 0.25, 1
16- animation = fadeIn, 1, 4, ease
17- animation = fadeOut, 1, 4, ease
18- animation = inputFieldDots, 1, 2, ease
19-}
20-
21-background {
22- monitor =
23- path = ~/.config/hypr/wallpaper.jpg
24- # Light blur + dim so the text stays readable over the leaves,
25- # like the blurred panel behind the login box.
26- blur_passes = 2
27- blur_size = 5
28- brightness = 0.75
29-}
30-
31-# Clock
32-label {
33- monitor = HDMI-A-2
34- text = <span font_weight="light">$TIME</span>
35- color = $text
36- font_family = $font
37- font_size = 110
38- position = 0, 170
39- halign = center
40- valign = center
41-}
42-
43-# Date
44-label {
45- monitor = HDMI-A-2
46- text = cmd[update:60000] date +"%A %-d %B"
47- color = rgba(238, 238, 238, 0.75)
48- font_family = $font
49- font_size = 18
50- position = 0, 70
51- halign = center
52- valign = center
53-}
54-
55-# Password box
56-input-field {
57- monitor = HDMI-A-2
58- size = 320, 50
59- rounding = 10
60- outline_thickness = 0
61- inner_color = $box
62- outer_color = rgba(0, 0, 0, 0)
63- check_color = rgba(170, 255, 255, 0.5)
64- fail_color = rgba(255, 107, 107, 0.6)
65- capslock_color = rgba(244, 214, 159, 0.5)
66- font_color = $text
67- font_family = $font
68- fade_on_empty = false
69- placeholder_text = <span foreground="##eeeeeeaa">Password</span>
70- fail_text = <span foreground="##eeeeee">$FAIL ($ATTEMPTS)</span>
71- dots_size = 0.22
72- dots_spacing = 0.4
73- dots_center = true
74- position = 0, -60
75- halign = center
76- valign = center
77-}
78-
79-# User
80-label {
81- monitor = HDMI-A-2
82- text = $USER
83- color = $text
84- font_family = $font
85- font_size = 15
86- position = 0, 0
87- halign = center
88- valign = center
89-}
deletedconfig/hypr/hyprpaper.conf
@@ -1,7 +1,0 @@
1-wallpaper {
2- monitor =
3- path = /home/vizie/Pictures/Wallpapers
4- timeout = 30
5-}
6-
7-splash = false
@@ -7,7 +7,7 @@
77 # Signal, Discord workspace 3, Signal on top and Discord below it
88 #
99 # Which workspace a window lands on (and Faugus' position) is decided by the
10-# temporary "startup-*" window rules in hyprland.lua, which also closes the window
10+# temporary "startup-*" window rules in startup.lua, which also closes the window
1111 # WowUp opens. This script starts the apps and then stacks Signal
1212 # above Discord. Apps that are already running are left alone, so it is safe to run
1313 # again by hand.
@@ -29,7 +29,7 @@
2929 done
3030
3131 # OpenDeck starts hidden with its flag. WowUp has one too but shows its window
32-# anyway; hyprland.lua closes that window (see "startup-wowup" there).
32+# anyway; startup.lua closes that window (see "startup-wowup" there).
3333 running '(^|/)\.?opendeck(-wrapped)? ' || launch opendeck --hide
3434 running 'wowup-cf' || launch wowup-cf --hidden
3535 running 'faugus\.(launcher|tray_only)' || launch faugus-launcher
@@ -1,0 +1,71 @@
1+-- Run once when Hyprland starts. Loaded from the hyprland.lua that Home Manager
2+-- writes (modules/home/desktop.nix has the rest of the Hyprland config).
3+
4+hl.on("hyprland.start", function()
5+ -- Wallpaper, bar, notifications, the control centre, tray helpers, the
6+ -- clipboard bridge and the polkit agent are systemd user services now
7+ -- (modules/home/services.nix in the dot repo): uwsm starts them with
8+ -- graphical-session.target, restarts them if they crash, and
9+ -- `systemctl --user status <name>` / `journalctl --user -u <name>` show them.
10+
11+ -- Apps opened at login, in their usual places (scripts/autostart-apps starts
12+ -- them, from autostart-apps.service). These placement rules are switched off
13+ -- again after two minutes, so opening the same apps by hand later behaves like
14+ -- any other window.
15+ local startup_rules = {
16+ hl.window_rule({
17+ name = "startup-signal",
18+ match = { class = "^signal$" },
19+ workspace = "3 silent",
20+ }),
21+ hl.window_rule({
22+ name = "startup-discord",
23+ match = { class = "^discord$" },
24+ workspace = "3 silent",
25+ }),
26+ hl.window_rule({
27+ name = "startup-faugus",
28+ match = { class = "^io\\.github\\.Faugus\\.faugus-launcher$" },
29+ workspace = "1 silent",
30+ float = true,
31+ size = { 430, 610 },
32+ move = { 1901, 333 },
33+ }),
34+ }
35+ hl.timer(function()
36+ for _, rule in ipairs(startup_rules) do
37+ rule:set_enabled(false)
38+ end
39+ end, { timeout = 120000, type = "oneshot" })
40+
41+ -- WowUp can't start hidden, so the window it opens at login is sent to a hidden
42+ -- workspace and closed again, which leaves it in the tray. That rule must not
43+ -- outlive the one window: it is dropped as soon as the window has opened (or
44+ -- after a minute if none came), so opening WowUp yourself right after login
45+ -- lands on your workspace like any other window.
46+ local wowup_rule = hl.window_rule({
47+ name = "startup-wowup",
48+ match = { class = "^WowUpCf$" },
49+ workspace = "special:autostart silent",
50+ })
51+ local wowup_pending = true
52+ local function wowup_done()
53+ wowup_pending = false
54+ wowup_rule:set_enabled(false)
55+ end
56+ hl.on("window.open", function(window)
57+ if wowup_pending and window.class == "WowUpCf" then
58+ wowup_done()
59+ -- closing it from inside the open event is too early and gets ignored
60+ local address = window.address
61+ hl.timer(function()
62+ hl.dispatch(hl.dsp.window.close({ window = "address:" .. address }))
63+ end, { timeout = 300, type = "oneshot" })
64+ end
65+ end)
66+ hl.timer(wowup_done, { timeout = 60000, type = "oneshot" })
67+
68+ -- hl.exec_cmd("gsettings set org.gnome.desktop.interface color-scheme prefer-dark")
69+ -- hl.exec_cmd("gsettings set org.gnome.desktop.interface gtk-theme Adwaita-dark")
70+end)
71+
deletedconfig/kde/kdeglobals
@@ -1,8 +1,0 @@
1-[General]
2-widgetStyle=kvantum
3-
4-[Icons]
5-Theme=Papirus-Dark
6-
7-[KDE]
8-widgetStyle=kvantum
deletedconfig/kitty/kitty.conf
@@ -1,29 +1,0 @@
1-background_opacity 0
2-font_size 12
3-font_family JetBrainsMono Nerd Font
4-bold_font JetBrains Mono Bold
5-italic_font JetBrains Mono Italic
6-bold_italic_font JetBrains Mono Bold Italic
7-disable_ligatures never
8-color0 #292d3e
9-color1 #ff8288
10-color2 #b4e88d
11-color3 #f4d69f
12-color4 #82aaff
13-color5 #e9c1ff
14-color6 #89ebff
15-color7 #d0d0d0
16-color8 #434758
17-color9 #ff8b92
18-color10 #ddffa7
19-color11 #ffe585
20-color12 #9cc4ff
21-color13 #ddb0f6
22-color14 #a3f7ff
23-color15 #ffffff
24-window_padding_width 10
25-term xterm-256color
26-# Do not restore the last window size or maximized state on startup; the tiling
27-# window manager decides the size. Otherwise one kitty closed while maximized makes
28-# every new one open maximized.
29-remember_window_size no
@@ -1,5 +1,5 @@
11 # RiceDark — Kvantum's bundled KvArcDark theme, unmodified except for the
2-# accent color, changed to match this rig's cyan (see hypr/hyprland.lua's
2+# accent color, changed to match this rig's cyan (see modules/home/desktop.nix's
33 # active_border rgb(aaffff)). Everything else is copied verbatim from
44 # KvArcDark since that's the theme actually confirmed to render correctly
55 # in Dolphin -- minimal diff from a known-good file, on purpose.
deletedconfig/kvantum/kvantum.kvconfig
@@ -1,2 +1,0 @@
1-[General]
2-theme=KvArcDark#
@@ -2,7 +2,7 @@
22
33 Same recipe as Waybar, fuzzel and the control centre: near-transparent black
44 over the compositor's blur, white text, no borders. The client's window is
5- transparent wherever this theme is, so decoration.blur in hypr/hyprland.lua
5+ transparent wherever this theme is, so decoration.blur (modules/home/desktop.nix)
66 does the frosting, exactly as it does for kitty.
77
88 Everything is under [data-desktop], so the same daemon opened in a web
deletedconfig/qt6ct/qt6ct.conf
@@ -1,27 +1,0 @@
1-[Appearance]
2-custom_palette=false
3-icon_theme=Papirus-Dark
4-standard_dialogs=default
5-style=kvantum
6-
7-[Fonts]
8-fixed="JetBrainsMono Nerd Font,10,-1,5,50,0,0,0,0,0"
9-general="Noto Sans,10,-1,5,50,0,0,0,0,0"
10-
11-[Interface]
12-activate_item_on_single_click=1
13-buttonbox_layout=0
14-cursor_flash_time=1000
15-dialog_buttons_have_icons=1
16-double_click_interval=400
17-gui_effects=@Invalid()
18-keyboard_scheme=2
19-menus_have_icons=true
20-show_shortcuts_in_context_menus=true
21-stylesheets=@Invalid()
22-toolbutton_style=4
23-underline_shortcut=1
24-wheel_scroll_lines=3
25-
26-[SettingsWindow]
27-geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\0\0\0\0\0\0\0\x4_\0\0\x5\x46\0\0\n\0\xff\xff\xfc|\0\0\vU\xff\xff\xfe\t\0\0\0\x1\x2\0\0\0\n\0\0\0\0\0\0\0\0\0\0\0\x4_\0\0\x5\x46)
@@ -4,7 +4,7 @@
44 black over the compositor's blur, white text, no borders, monochrome highlights.
55 Signal has no user stylesheet, so signal/launch builds a copy of the app with this
66 file added as stylesheets/custom.css and the main window made transparent;
7- decoration.blur in hypr/hyprland.lua then does the frosting, as it does for kitty.
7+ decoration.blur (modules/home/desktop.nix) then does the frosting, as it does for kitty.
88
99 Signal draws nearly everything from its --axo-color-* variables, so most of the
1010 theme is just new values for those. It assumes Signal's dark theme (Settings >
deletedconfig/waybar/config
@@ -1,94 +1,0 @@
1-[
2- {
3- "clock": {
4- "format": "{:%H:%M}",
5- "format-alt": "{:%A, %B %d, %Y (%R)}",
6- "tooltip-format": "<tt><big>{calendar}</big></tt>",
7- "calendar": {
8- "mode" : "month",
9- "iso8601" : true,
10- "mode-mon-col" : 3,
11- "weeks-pos" : "right",
12- "on-scroll" : 1,
13- "format": {
14- "months": "<span color='#ffffff'><b>{}</b></span>",
15- "days": "<span color='#ffffff'>{}</span>",
16- "weeks": "<span color='#8a8a8a'>W{:%V}</span>",
17- "weekdays": "<span color='#b0b0b0'><b>{}</b></span>",
18- "today": "<span color='#aaffff'><b><u>{}</u></b></span>"
19- }
20- },
21- "actions": {
22- "on-click-right": "mode",
23- "on-scroll-up": "shift_up",
24- "on-scroll-down": "shift_down"
25- }
26- },
27- "custom/notifications": {
28- "format": "<span size='12288'>󰂚</span>",
29- "tooltip": false,
30- "on-click": "bash $HOME/.config/waybar/eww/scripts/toggle.sh notifications"
31- },
32- "custom/notifications-count": {
33- "exec": "python3 $HOME/.config/waybar/scripts/notification-count.py",
34- "return-type": "json",
35- "hide-empty-text": true,
36- "tooltip": false,
37- "on-click": "bash $HOME/.config/waybar/eww/scripts/toggle.sh notifications"
38- },
39- "custom/control-center": {
40- "format": " ",
41- "tooltip": false,
42- "on-click": "bash $HOME/.config/waybar/eww/scripts/toggle.sh"
43- },
44- "tray": {
45- "icon-size": 16,
46- "spacing": 10,
47- "order": ["discord_status_icon_1"],
48- "icons": {
49- "Signal_status_icon_1": "/home/vizie/.config/waybar/icons/signal.svg",
50- "Bitwarden_status_icon_1": "/home/vizie/.config/waybar/icons/bitwarden.svg",
51- "WowUpCf_status_icon_1": "/home/vizie/.config/waybar/icons/wowup.svg",
52- "Claude_status_icon_1": "/home/vizie/.config/waybar/icons/claude.svg",
53- "steam_app_default": "/home/vizie/.config/waybar/icons/battlenet.svg",
54- "faugus-launcher": "/home/vizie/.config/waybar/icons/faugus.svg",
55- "steam": "/home/vizie/.config/waybar/icons/steam.svg",
56- "tray-icon tray app opendeck": "/home/vizie/.config/waybar/icons/opendeck.svg"
57- }
58- },
59- "custom/imsh-cast-monitor": {
60- "format": "{text}",
61- "exec": "imsh-cast-monitor",
62- "return-type": "json",
63- "hide-empty-text": true
64- },
65- "height": 36,
66- "hyprland/window": {
67- "max-length": 60
68- },
69- "hyprland/workspaces": {
70- "format": "{name}",
71- "on-click": "activate",
72- "sort-by": "number"
73- },
74- "layer": "top",
75- "modules-center": [
76- "custom/imsh-cast-monitor",
77- "clock"
78- ],
79- "modules-left": [
80- "hyprland/workspaces",
81- "hyprland/window"
82- ],
83- "modules-right": [
84- "tray",
85- "custom/notifications",
86- "custom/notifications-count",
87- "custom/control-center"
88- ],
89- "position": "top",
90- "margin-left": 15,
91- "margin-right": 15,
92- "margin-top": 15
93- }
94-]
@@ -22,7 +22,7 @@
2222
2323 .cc {
2424 // Same near-transparent black as #waybar / fuzzel's background; the
25- // compositor's own blur (layerrule-4 in hyprland.lua) does the rest, no
25+ // compositor's own blur (layerrule-4 in modules/home/desktop.nix) does the rest, no
2626 // border, same as the rest of the rice.
2727 background-color: rgba(0, 0, 0, 0.1);
2828 border-radius: 20px;
deletedconfig/waybar/style.css
@@ -1,114 +1,0 @@
1-* {
2- border: none;
3- border-radius: 17px;
4- font-family: NotoSans Nerd Font Regular;
5- font-size: 12px;
6- font-weight: bolder;
7- background: transparent;
8- color: white;
9- min-height: 0;
10-}
11-
12-window#waybar {
13- background: rgba(0, 0, 0, 0.1);
14-}
15-
16-window#waybar #tray menu{
17- background: rgba(0, 0, 0, 0.1);
18-}
19-
20-#tray menu :hover {
21- background: rgba(0, 0, 0, 0.1);
22-}
23-
24-#tray menu :hover label {
25- background: rgba(0, 0, 0, 0.1);
26-}
27-
28-tooltip {
29- background: rgba(0, 0, 0, 0.1);
30-}
31-
32-/* Workspace buttons are pills inset 4px from the bar's edge, so their 13px radius
33- is concentric with the bar's 17px one. Everything GTK's theme draws on a button
34- (border, shadows, hover gradient) is reset here, as it otherwise shows up as a
35- square box on hover. */
36-#workspaces button {
37- margin: 4px 0 4px 4px;
38- padding: 0 6px;
39- min-width: 16px;
40- border: none;
41- border-radius: 13px;
42- outline: none;
43- box-shadow: none;
44- text-shadow: none;
45- background-color: transparent;
46- /* Active-workspace underline: 2px line that grows from the centre (see .active). */
47- background-image: linear-gradient(white, white);
48- background-repeat: no-repeat;
49- background-position: 50% 92%;
50- background-size: 0 2px;
51- transition: background-size 200ms ease-out, background-color 150ms ease-out;
52-}
53-
54-#workspaces button:hover {
55- background-color: rgba(255, 255, 255, 0.12);
56-}
57-
58-#tray window decoration{
59- border-style: none;
60- box-shadow: none;
61-}
62-
63-#workspaces button.active {
64- background-size: 10px 2px;
65-}
66-
67-/* Workspaces with no windows are dimmed, unless it's the one you're on. The colour
68- goes on the label because the `*` rule above gives every label its own white. */
69-#workspaces button label {
70- transition: color 150ms ease-out;
71-}
72-
73-#workspaces button.empty:not(.active) label {
74- color: rgba(255, 255, 255, 0.4);
75-}
76-
77-#clock, #tray, #custom-notifications, #custom-control-center {
78- padding: 0 7px;
79-}
80-
81-/* Notification count: a small white disc overlapping the bell's top right corner.
82- The margins shrink it from the bar's full height to 12px; the negative left margin
83- pulls it over the bell. Hidden by Waybar when there are no notifications. */
84-#custom-notifications-count {
85- min-width: 6px;
86- padding: 0 3px;
87- margin: 7px 3px 17px -11px;
88- border-radius: 6px;
89- background: white;
90- color: #18181c;
91- font-size: 8px;
92-}
93-
94-.imsh-cast-monitor.recording {
95- color: #f00;
96-}
97-
98-.imsh-cast-monitor.error {
99- color: #ff0;
100-}
101-
102-/* Control-center button: your logo as a pre-rendered 18px image.
103- GTK stretches SVG backgrounds as pixels (which makes them blurry), so logo-18.png is
104- rendered at exactly 18px, and logo-36.png is used automatically on 2x-scaled monitors. */
105-#custom-control-center {
106- min-width: 18px;
107- min-height: 18px;
108- background-image: -gtk-scaled(url("file:///home/vizie/.config/waybar/logo-18.png"),
109- url("file:///home/vizie/.config/waybar/logo-36.png"));
110- background-repeat: no-repeat;
111- background-position: center;
112- background-size: 18px 18px;
113- margin-right: 6px; /* gap between the logo and the end of the bar */
114-}
@@ -1,0 +1,151 @@
1+# Settings for the apps you use directly: terminal, launcher, editor, browser.
2+{
3+ config,
4+ lib,
5+ ...
6+}:
7+let
8+ inherit (config.lib.dot) link;
9+in
10+{
11+ programs.kitty = {
12+ enable = true;
13+ settings = {
14+ background_opacity = 0;
15+ font_size = 12;
16+ # JetBrains Mono isn't installed (see fonts.packages in
17+ # modules/nixos/desktop.nix), so kitty falls back to Noto Sans Mono.
18+ font_family = "JetBrainsMono Nerd Font";
19+ bold_font = "JetBrains Mono Bold";
20+ italic_font = "JetBrains Mono Italic";
21+ bold_italic_font = "JetBrains Mono Bold Italic";
22+ disable_ligatures = "never";
23+ window_padding_width = 10;
24+ term = "xterm-256color";
25+ # Do not restore the last window size or maximized state on startup; the
26+ # tiling window manager decides the size. Otherwise one kitty closed while
27+ # maximized makes every new one open maximized.
28+ remember_window_size = false;
29+
30+ color0 = "#292d3e";
31+ color1 = "#ff8288";
32+ color2 = "#b4e88d";
33+ color3 = "#f4d69f";
34+ color4 = "#82aaff";
35+ color5 = "#e9c1ff";
36+ color6 = "#89ebff";
37+ color7 = "#d0d0d0";
38+ color8 = "#434758";
39+ color9 = "#ff8b92";
40+ color10 = "#ddffa7";
41+ color11 = "#ffe585";
42+ color12 = "#9cc4ff";
43+ color13 = "#ddb0f6";
44+ color14 = "#a3f7ff";
45+ color15 = "#ffffff";
46+ };
47+ };
48+
49+ # Hyprland's mod + Return runs config/fuzzel/launch (linked in files.nix),
50+ # which puts the apps in config/fuzzel/favorites at the top.
51+ programs.fuzzel = {
52+ enable = true;
53+ settings = {
54+ main = {
55+ font = "NotoSansM Nerd Font Propo:size=12";
56+ # Size fonts like GTK apps do (96 DPI) rather than by the monitors' real
57+ # DPI, which made everything bigger.
58+ dpi-aware = "no";
59+ terminal = lib.getExe config.programs.kitty.package;
60+ # Same icon theme as Qt/KDE (theming.nix). Without it fuzzel mixes each
61+ # app's own icon, in different styles and sizes.
62+ icon-theme = config.qt.qt6ctSettings.Appearance.icon_theme;
63+ list-executables-in-path = "yes";
64+ placeholder = "Search...";
65+ line-height = 36;
66+ lines = 5;
67+ width = 45;
68+ horizontal-pad = 25;
69+ vertical-pad = 25;
70+ inner-pad = 10;
71+ };
72+
73+ # RRGGBBAA. Same near-transparent black as Waybar and the control centre,
74+ # over Hyprland's blur (the "launcher" layer rule).
75+ colors = {
76+ background = "0000001a";
77+ text = "f2f2f2cc";
78+ prompt = "f2f2f2cc";
79+ input = "f2f2f2ff";
80+ placeholder = "f2f2f2cc";
81+ match = "f4d69fff";
82+ selection = "0000001a";
83+ selection-text = "f2f2f2ff";
84+ selection-match = "f4d69fff";
85+ counter = "f2f2f2cc";
86+ };
87+
88+ # Panel 20, rows inside it 10, like the control centre.
89+ border = {
90+ width = 0;
91+ radius = 20;
92+ selection-radius = 10;
93+ };
94+ };
95+ };
96+
97+ # config/nvim/init.lua. With liveConfig it is loaded from the checkout, so an
98+ # edit applies the next time nvim starts.
99+ programs.neovim = {
100+ enable = true;
101+ withRuby = false;
102+ withPython3 = false;
103+ initLua =
104+ if config.dot.liveConfig then
105+ ''dofile("${config.dot.repoPath}/config/nvim/init.lua")''
106+ else
107+ builtins.readFile ../../config/nvim/init.lua;
108+ };
109+
110+ # Firefox: the profile restored from CachyOS, kept by its existing folder and
111+ # StoreID (its Profile Groups database). Nixpkgs' Firefox runs with
112+ # MOZ_LEGACY_PROFILES, so it opens the profile marked default here. Firefox
113+ # itself comes from the system (programs.firefox in modules/nixos/apps.nix).
114+ programs.firefox = {
115+ enable = true;
116+ package = null;
117+ profiles.default-release = {
118+ path = "3b2nokq1.default-release";
119+ storeId = "be5ac646";
120+ # Written to user.js, which Firefox reads at startup. Setting userChrome
121+ # also turns on toolkit.legacyUserProfileCustomizations.stylesheets.
122+ settings = {
123+ # Restore the previous session.
124+ "browser.startup.page" = 3;
125+ "browser.sessionstore.resume_from_crash" = true;
126+ # Middle mouse scroll.
127+ "general.autoScroll" = true;
128+ "middlemouse.paste" = false;
129+ "general.aboutConfig.enable" = true;
130+ "sidebar.animation.enabled" = false;
131+ # "image.jxl.enabled" = true;
132+
133+ # Browser toolbox and devtools.
134+ "devtools.chrome.enabled" = true;
135+ "devtools.debugger.remote-enabled" = true;
136+ "devtools.cache.disabled" = true;
137+ "devtools.netmonitor.persistlog" = true;
138+ "devtools.webconsole.persistlog" = true;
139+ "devtools.webconsole.timestampMessages" = true;
140+
141+ # Hardware video decoding on NVIDIA via VA-API (libva-nvidia-driver).
142+ # Firefox blocks NVIDIA for this by default; needs NVD_BACKEND and
143+ # MOZ_DISABLE_RDD_SANDBOX from Hyprland's env (desktop.nix) as well.
144+ "media.hardware-video-decoding.force-enabled" = true;
145+ "media.ffmpeg.vaapi.enabled" = true;
146+ "widget.dmabuf.force-enabled" = true;
147+ };
148+ userChrome = link "firefox/userChrome.css";
149+ };
150+ };
151+}
@@ -7,6 +7,8 @@
77 {
88 imports = [
99 ./files.nix
10+ ./desktop.nix
11+ ./apps.nix
1012 ./services.nix
1113 ./shell.nix
1214 ./theming.nix
@@ -24,16 +26,26 @@
2426 type = lib.types.bool;
2527 default = true;
2628 description = ''
27- Link the hand-edited configs (hypr, waybar, kitty, ...) straight to the
28- checkout at repoPath, as the old symlink.sh did: an edit takes effect
29- the next time the program reads it, with no rebuild. Off, they are
30- copied into the Nix store and change only on a rebuild (the test VM
31- does this, since it has no checkout).
29+ Link the hand-edited configs that no module writes (scripts, Eww,
30+ startup.lua, init.lua, stylesheets, themes) straight to the checkout at
31+ repoPath, as the old symlink.sh did: an edit takes effect the next time
32+ the program reads it, with no rebuild. Off, they are copied into the
33+ Nix store and change only on a rebuild (the test VM does this, since it
34+ has no checkout).
3235 '';
3336 };
3437 };
3538
3639 config = {
40+ # A file or directory of config/: linked to the checkout when liveConfig is
41+ # on (edits apply at once), copied into the store otherwise.
42+ lib.dot.link =
43+ rel:
44+ if config.dot.liveConfig then
45+ config.lib.file.mkOutOfStoreSymlink "${config.dot.repoPath}/config/${rel}"
46+ else
47+ ../../config + "/${rel}";
48+
3749 home = {
3850 username = "vizie";
3951 homeDirectory = "/home/vizie";
@@ -1,0 +1,704 @@
1+# The Hyprland desktop: compositor, lock screen, idle and wallpaper daemons, the
2+# bar and notifications. Hyprland and hyprlock themselves come from the NixOS
3+# modules (modules/nixos/desktop.nix); these write their configs.
4+#
5+# The services (hypridle, hyprpaper, waybar, dunst) run with
6+# graphical-session.target, which uwsm starts; services.nix puts them in uwsm's
7+# session slice.
8+{
9+ config,
10+ lib,
11+ pkgs,
12+ ...
13+}:
14+let
15+ inherit (config.lib.dot) link;
16+
17+ lua = lib.generators.mkLuaInline;
18+ toLua = lib.generators.toLua { };
19+
20+ configHome = config.xdg.configHome;
21+ wallpaper = ../../config/hypr/wallpaper.jpg;
22+
23+ # --- Hyprland -----------------------------------------------------------------
24+
25+ mod = "SUPER";
26+ bind = keys: action: {
27+ _args = [
28+ "${mod} + ${keys}"
29+ (lua action)
30+ ];
31+ };
32+ exec = command: "hl.dsp.exec_cmd(${toLua command})";
33+ screenshotKeys = "${configHome}/keys/scrnis";
34+
35+ # hl.dsp.dpms for hypridle, which runs it through hyprctl.
36+ dpms = action: "hyprctl dispatch 'hl.dsp.dpms({ action = \"${action}\" })'";
37+
38+ # --- Waybar -------------------------------------------------------------------
39+
40+ toggleFlyout = "${pkgs.bash}/bin/bash ${configHome}/waybar/eww/scripts/toggle.sh";
41+ trayIcons = ../../config/waybar/icons;
42+in
43+{
44+ wayland.windowManager.hyprland = {
45+ enable = true;
46+ # From the NixOS module (programs.hyprland), which uwsm starts. uwsm also
47+ # brings up graphical-session.target, so Home Manager's own session target
48+ # isn't needed.
49+ package = null;
50+ portalPackage = null;
51+ systemd.enable = false;
52+ configType = "lua";
53+
54+ settings = {
55+ monitor = [
56+ {
57+ output = "HDMI-A-2";
58+ mode = "2560x1440@280";
59+ position = "0x0";
60+ scale = 1;
61+ vrr = 0;
62+ }
63+ {
64+ output = "DP-1";
65+ mode = "2560x1440@144";
66+ position = "2560x-900";
67+ scale = 1;
68+ transform = 1;
69+ vrr = 0;
70+ }
71+ ];
72+
73+ # Workspaces 1-2 on HDMI-A-2, 3-5 on DP-1.
74+ workspace_rule = map (workspace: {
75+ workspace = toString workspace;
76+ monitor = if workspace <= 2 then "HDMI-A-2" else "DP-1";
77+ persistent = true;
78+ }) (lib.range 1 5);
79+
80+ window_rule = [
81+ {
82+ name = "windowrule-1";
83+ match.title = "^RuneLite.*";
84+ float = true;
85+ }
86+ {
87+ name = "windowrule-2";
88+ match = {
89+ class = "net-runelite-client-RuneLite";
90+ title = "win.*";
91+ };
92+ no_focus = true;
93+ }
94+ {
95+ name = "windowrule-3";
96+ match.class = ".*";
97+ immediate = true;
98+ }
99+ # Battle.net (Proton via Faugus). The class is shared by every umu game
100+ # without its own ID, so match on the title too.
101+ {
102+ name = "battlenet-float";
103+ match = {
104+ class = "^steam_app_default$";
105+ title = "^Battle\\.net.*";
106+ };
107+ float = true;
108+ }
109+ # xembedsniproxy (pkgs/xembedsniproxy) puts Wine/Proton tray icons such
110+ # as Battle.net into Waybar's tray. It keeps a small helper window mapped
111+ # for the icons it hosts, which Hyprland would otherwise show as a
112+ # floating window.
113+ {
114+ name = "xembedsniproxy";
115+ match.class = "^xembedsniproxy$";
116+ opacity = "0.0 override 0.0 override";
117+ border_size = 0;
118+ no_blur = true;
119+ no_shadow = true;
120+ no_anim = true;
121+ no_focus = true;
122+ no_initial_focus = true;
123+ }
124+ # Dolphin's transparency comes from Kvantum's translucent_windows (see
125+ # config/kvantum/RiceDark.kvconfig), not a forced window opacity: Kvantum
126+ # puts alpha in the background only, so text and icons stay opaque, as
127+ # in kitty. decoration.blur blurs through it. If Dolphin looks opaque
128+ # after applying the Kvantum theme, this rule can come back:
129+ # { name = "windowrule-4"; match.class = "^(org.kde.dolphin)$"; opacity = "0.90 0.80"; }
130+
131+ # Claude's desktop app creates its window opaque (transparent: false in
132+ # the Electron bundle) and has no user stylesheet, so unlike kitty and
133+ # Dolphin it can't put alpha in the background only. A forced window
134+ # opacity is the closest it gets: decoration.blur frosts what shows
135+ # through, at the cost of the text being translucent too.
136+ {
137+ name = "claude-frosted";
138+ match.class = "^com\\.anthropic\\.Claude$";
139+ opacity = "0.80 0.80";
140+ }
141+ ];
142+
143+ # Blur behind the translucent layers: Waybar, fuzzel ("launcher"), dunst
144+ # and the control centre.
145+ layer_rule = [
146+ {
147+ name = "layerrule-1";
148+ match.namespace = "waybar";
149+ blur = true;
150+ ignore_alpha = 0;
151+ blur_popups = true;
152+ }
153+ {
154+ name = "layerrule-2";
155+ match.namespace = "launcher";
156+ ignore_alpha = 0;
157+ blur = true;
158+ }
159+ {
160+ name = "layerrule-3";
161+ match.namespace = "notifications";
162+ ignore_alpha = 0;
163+ blur = true;
164+ }
165+ {
166+ name = "layerrule-4";
167+ match.namespace = "eww-control-center";
168+ ignore_alpha = 0;
169+ blur = true;
170+ }
171+ ];
172+
173+ bind = [
174+ (bind "SHIFT + S" (exec "imsh-shot area -Cufa@${screenshotKeys} -s1"))
175+ (bind "SHIFT + F" (exec "imsh-shot screen -Cua@${screenshotKeys}"))
176+ (bind "SHIFT + R" (exec "imsh-cast -Cua@${screenshotKeys}"))
177+ (bind "Return" (exec "${configHome}/fuzzel/launch"))
178+ (bind "T" (exec (lib.getExe config.programs.kitty.package)))
179+ (bind "W" (exec "firefox"))
180+ (bind "Q" "hl.dsp.window.close()")
181+ (bind "F" ''hl.dsp.window.float({ action = "toggle" })'')
182+ (bind "G" ''hl.dsp.window.fullscreen({ mode = "fullscreen", action = "toggle" })'')
183+ (bind "mouse_down" ''hl.dsp.focus({ workspace = "m+1" })'')
184+ (bind "mouse_up" ''hl.dsp.focus({ workspace = "m-1" })'')
185+ (bind "mouse:272" "hl.dsp.window.drag()")
186+ (bind "mouse:273" "hl.dsp.window.resize()")
187+ (bind "Delete" "hl.dsp.exit()")
188+ ];
189+
190+ env =
191+ lib.mapAttrsToList
192+ (name: value: {
193+ _args = [
194+ name
195+ value
196+ ];
197+ })
198+ {
199+ LIBVA_DRIVER_NAME = "nvidia";
200+ # libva-nvidia-driver's backend, for Firefox's hardware video decoding.
201+ NVD_BACKEND = "direct";
202+ # Lets Firefox's video decoder process reach the NVIDIA driver.
203+ MOZ_DISABLE_RDD_SANDBOX = "1";
204+ XDG_SESSION_TYPE = "wayland";
205+ GBM_BACKEND = "nvidia-drm";
206+ __GLX_VENDOR_LIBRARY_NAME = "nvidia";
207+ XCURSOR_THEME = config.home.pointerCursor.name;
208+ XCURSOR_SIZE = toString config.home.pointerCursor.size;
209+ QT_QPA_PLATFORMTHEME = config.qt.platformTheme.name;
210+ };
211+
212+ config = {
213+ general = {
214+ border_size = 1;
215+ gaps_in = 5;
216+ gaps_out = 17;
217+ layout = "dwindle";
218+ col = {
219+ active_border = "rgba(00000000)";
220+ inactive_border = "rgba(00000000)";
221+ };
222+ allow_tearing = true;
223+ };
224+ decoration = {
225+ rounding = 10;
226+ # No visible borders; the focused window is the one that isn't dimmed.
227+ dim_inactive = true;
228+ dim_strength = 0.15;
229+ blur = {
230+ enabled = true;
231+ size = 9;
232+ passes = 3;
233+ new_optimizations = true;
234+ # Blur tooltips and context menus (Kvantum's popup_blurring is on for this).
235+ popups = true;
236+ };
237+ shadow = {
238+ enabled = false;
239+ render_power = 3;
240+ range = 4;
241+ color = "rgba(1a1a1aee)";
242+ };
243+ };
244+ input = {
245+ kb_layout = "no";
246+ follow_mouse = 1;
247+ sensitivity = 0;
248+ accel_profile = "flat";
249+ };
250+ misc = {
251+ disable_hyprland_logo = true;
252+ disable_splash_rendering = true;
253+ # The wallpaper's average colour instead of black, so the moment
254+ # between the login screen and hyprpaper drawing the wallpaper doesn't
255+ # flash black.
256+ background_color = "rgb(121d1a)";
257+ };
258+ ecosystem.no_donation_nag = true;
259+ binds.scroll_event_delay = 10;
260+ # Software cursor, for tearing. Hyprland (0.56) refuses to tear while a
261+ # hardware cursor is visible, because the kernel can't update the cursor
262+ # plane during a tearing flip. So with a hardware cursor a game only
263+ # tears while it hides the pointer (e.g. mouselook) and is vsynced
264+ # otherwise, and switching back and forth left WoW's cursor shape stale.
265+ # With a software cursor the game tears all the time; the price is that
266+ # frames are composited (one extra copy) while the pointer is visible.
267+ cursor.no_hardware_cursors = true;
268+ # Fullscreen games (e.g. WoW) scan out straight to the monitor instead
269+ # of being copied by the compositor. With the software cursor above this
270+ # only applies while the game hides the pointer. 1 = always; XWayland
271+ # games don't set a content type, so 2 (auto) won't trigger.
272+ render.direct_scanout = 1;
273+ };
274+ };
275+
276+ # What happens at login (the apps' placement rules), in Lua: linked to the
277+ # checkout like the other hand-edited configs.
278+ extraLuaFiles.startup = link "hypr/startup.lua";
279+ };
280+
281+ # --- Lock screen, idle, wallpaper -----------------------------------------------
282+
283+ # Styled to match the greetd login screen (config/greetd/style.css): same
284+ # bamboo wallpaper, translucent black boxes, #eee text, rounding 10.
285+ programs.hyprlock = {
286+ enable = true;
287+ # From the NixOS module (programs.hyprlock), which also sets up its PAM.
288+ package = null;
289+ settings =
290+ let
291+ centered = {
292+ monitor = "HDMI-A-2";
293+ halign = "center";
294+ valign = "center";
295+ };
296+ in
297+ {
298+ "$font" = "NotoSans Nerd Font";
299+ "$text" = "rgb(238, 238, 238)";
300+ "$box" = "rgba(0, 0, 0, 0.2)";
301+
302+ general = {
303+ hide_cursor = false;
304+ ignore_empty_input = true;
305+ };
306+
307+ animations = {
308+ enabled = true;
309+ bezier = "ease, 0.25, 0.1, 0.25, 1";
310+ animation = [
311+ "fadeIn, 1, 4, ease"
312+ "fadeOut, 1, 4, ease"
313+ "inputFieldDots, 1, 2, ease"
314+ ];
315+ };
316+
317+ # Light blur and dim so the text stays readable over the leaves, like
318+ # the blurred panel behind the login box.
319+ background = {
320+ monitor = "";
321+ path = "${wallpaper}";
322+ blur_passes = 2;
323+ blur_size = 5;
324+ brightness = 0.75;
325+ };
326+
327+ label = [
328+ # Clock
329+ (
330+ centered
331+ // {
332+ text = ''<span font_weight="light">$TIME</span>'';
333+ color = "$text";
334+ font_family = "$font";
335+ font_size = 110;
336+ position = "0, 170";
337+ }
338+ )
339+ # Date
340+ (
341+ centered
342+ // {
343+ text = ''cmd[update:60000] date +"%A %-d %B"'';
344+ color = "rgba(238, 238, 238, 0.75)";
345+ font_family = "$font";
346+ font_size = 18;
347+ position = "0, 70";
348+ }
349+ )
350+ # User
351+ (
352+ centered
353+ // {
354+ text = "$USER";
355+ color = "$text";
356+ font_family = "$font";
357+ font_size = 15;
358+ position = "0, 0";
359+ }
360+ )
361+ ];
362+
363+ input-field = centered // {
364+ size = "320, 50";
365+ rounding = 10;
366+ outline_thickness = 0;
367+ inner_color = "$box";
368+ outer_color = "rgba(0, 0, 0, 0)";
369+ check_color = "rgba(170, 255, 255, 0.5)";
370+ fail_color = "rgba(255, 107, 107, 0.6)";
371+ capslock_color = "rgba(244, 214, 159, 0.5)";
372+ font_color = "$text";
373+ font_family = "$font";
374+ fade_on_empty = false;
375+ placeholder_text = ''<span foreground="##eeeeeeaa">Password</span>'';
376+ fail_text = ''<span foreground="##eeeeee">$FAIL ($ATTEMPTS)</span>'';
377+ dots_size = 0.22;
378+ dots_spacing = 0.4;
379+ dots_center = true;
380+ position = "0, -60";
381+ };
382+ };
383+ };
384+
385+ services.hypridle = {
386+ enable = true;
387+ settings = {
388+ general = {
389+ lock_cmd = "pidof hyprlock || hyprlock";
390+ before_sleep_cmd = "loginctl lock-session";
391+ after_sleep_cmd = dpms "on";
392+ };
393+ listener = [
394+ # Lock after 10 minutes idle (respects inhibitors, e.g. fullscreen video).
395+ {
396+ timeout = 600;
397+ on-timeout = "loginctl lock-session";
398+ }
399+ {
400+ timeout = 1;
401+ on-resume = dpms "on";
402+ on-timeout = "true";
403+ ignore_inhibit = true;
404+ }
405+ {
406+ timeout = 300;
407+ on-timeout = dpms "off";
408+ ignore_inhibit = true;
409+ }
410+ ];
411+ };
412+ };
413+
414+ # A new picture from the folder every 30 seconds, on every monitor.
415+ services.hyprpaper = {
416+ enable = true;
417+ settings = {
418+ wallpaper = {
419+ monitor = "";
420+ path = "${config.xdg.userDirs.pictures}/Wallpapers";
421+ timeout = 30;
422+ };
423+ splash = false;
424+ };
425+ };
426+
427+ # --- Waybar ---------------------------------------------------------------------
428+
429+ # The flyouts it opens (control centre, notifications) are Eww windows, in
430+ # config/waybar/eww; its scripts are in config/waybar/scripts. Both stay linked
431+ # to the checkout (files.nix).
432+ programs.waybar = {
433+ enable = true;
434+ systemd.enable = true;
435+
436+ settings.main = {
437+ layer = "top";
438+ position = "top";
439+ height = 36;
440+ margin-left = 15;
441+ margin-right = 15;
442+ margin-top = 15;
443+
444+ modules-left = [
445+ "hyprland/workspaces"
446+ "hyprland/window"
447+ ];
448+ modules-center = [
449+ "custom/imsh-cast-monitor"
450+ "clock"
451+ ];
452+ modules-right = [
453+ "tray"
454+ "custom/notifications"
455+ "custom/notifications-count"
456+ "custom/control-center"
457+ ];
458+
459+ "hyprland/workspaces" = {
460+ format = "{name}";
461+ on-click = "activate";
462+ sort-by = "number";
463+ };
464+ "hyprland/window".max-length = 60;
465+
466+ "custom/imsh-cast-monitor" = {
467+ format = "{text}";
468+ exec = "imsh-cast-monitor";
469+ return-type = "json";
470+ hide-empty-text = true;
471+ };
472+
473+ clock = {
474+ format = "{:%H:%M}";
475+ format-alt = "{:%A, %B %d, %Y (%R)}";
476+ tooltip-format = "<tt><big>{calendar}</big></tt>";
477+ calendar = {
478+ mode = "month";
479+ iso8601 = true;
480+ mode-mon-col = 3;
481+ weeks-pos = "right";
482+ on-scroll = 1;
483+ format = {
484+ months = "<span color='#ffffff'><b>{}</b></span>";
485+ days = "<span color='#ffffff'>{}</span>";
486+ weeks = "<span color='#8a8a8a'>W{:%V}</span>";
487+ weekdays = "<span color='#b0b0b0'><b>{}</b></span>";
488+ today = "<span color='#aaffff'><b><u>{}</u></b></span>";
489+ };
490+ };
491+ actions = {
492+ on-click-right = "mode";
493+ on-scroll-up = "shift_up";
494+ on-scroll-down = "shift_down";
495+ };
496+ };
497+
498+ # Own icons for the tray apps, by their StatusNotifierItem ID.
499+ tray = {
500+ icon-size = 16;
501+ spacing = 10;
502+ order = [ "discord_status_icon_1" ];
503+ icons = {
504+ Signal_status_icon_1 = "${trayIcons}/signal.svg";
505+ Bitwarden_status_icon_1 = "${trayIcons}/bitwarden.svg";
506+ WowUpCf_status_icon_1 = "${trayIcons}/wowup.svg";
507+ Claude_status_icon_1 = "${trayIcons}/claude.svg";
508+ steam_app_default = "${trayIcons}/battlenet.svg";
509+ faugus-launcher = "${trayIcons}/faugus.svg";
510+ steam = "${trayIcons}/steam.svg";
511+ "tray-icon tray app opendeck" = "${trayIcons}/opendeck.svg";
512+ };
513+ };
514+
515+ "custom/notifications" = {
516+ format = "<span size='12288'>󰂚</span>";
517+ tooltip = false;
518+ on-click = "${toggleFlyout} notifications";
519+ };
520+ "custom/notifications-count" = {
521+ exec = "${pkgs.python3-desktop}/bin/python3 ${configHome}/waybar/scripts/notification-count.py";
522+ return-type = "json";
523+ hide-empty-text = true;
524+ tooltip = false;
525+ on-click = "${toggleFlyout} notifications";
526+ };
527+ "custom/control-center" = {
528+ format = " ";
529+ tooltip = false;
530+ on-click = toggleFlyout;
531+ };
532+ };
533+
534+ style = ''
535+ * {
536+ border: none;
537+ border-radius: 17px;
538+ font-family: NotoSans Nerd Font Regular;
539+ font-size: 12px;
540+ font-weight: bolder;
541+ background: transparent;
542+ color: white;
543+ min-height: 0;
544+ }
545+
546+ window#waybar {
547+ background: rgba(0, 0, 0, 0.1);
548+ }
549+
550+ window#waybar #tray menu {
551+ background: rgba(0, 0, 0, 0.1);
552+ }
553+
554+ #tray menu :hover {
555+ background: rgba(0, 0, 0, 0.1);
556+ }
557+
558+ #tray menu :hover label {
559+ background: rgba(0, 0, 0, 0.1);
560+ }
561+
562+ tooltip {
563+ background: rgba(0, 0, 0, 0.1);
564+ }
565+
566+ /* Workspace buttons are pills inset 4px from the bar's edge, so their 13px radius
567+ is concentric with the bar's 17px one. Everything GTK's theme draws on a button
568+ (border, shadows, hover gradient) is reset here, as it otherwise shows up as a
569+ square box on hover. */
570+ #workspaces button {
571+ margin: 4px 0 4px 4px;
572+ padding: 0 6px;
573+ min-width: 16px;
574+ border: none;
575+ border-radius: 13px;
576+ outline: none;
577+ box-shadow: none;
578+ text-shadow: none;
579+ background-color: transparent;
580+ /* Active-workspace underline: 2px line that grows from the centre (see .active). */
581+ background-image: linear-gradient(white, white);
582+ background-repeat: no-repeat;
583+ background-position: 50% 92%;
584+ background-size: 0 2px;
585+ transition: background-size 200ms ease-out, background-color 150ms ease-out;
586+ }
587+
588+ #workspaces button:hover {
589+ background-color: rgba(255, 255, 255, 0.12);
590+ }
591+
592+ #tray window decoration {
593+ border-style: none;
594+ box-shadow: none;
595+ }
596+
597+ #workspaces button.active {
598+ background-size: 10px 2px;
599+ }
600+
601+ /* Workspaces with no windows are dimmed, unless it's the one you're on. The colour
602+ goes on the label because the `*` rule above gives every label its own white. */
603+ #workspaces button label {
604+ transition: color 150ms ease-out;
605+ }
606+
607+ #workspaces button.empty:not(.active) label {
608+ color: rgba(255, 255, 255, 0.4);
609+ }
610+
611+ #clock, #tray, #custom-notifications, #custom-control-center {
612+ padding: 0 7px;
613+ }
614+
615+ /* Notification count: a small white disc overlapping the bell's top right corner.
616+ The margins shrink it from the bar's full height to 12px; the negative left margin
617+ pulls it over the bell. Hidden by Waybar when there are no notifications. */
618+ #custom-notifications-count {
619+ min-width: 6px;
620+ padding: 0 3px;
621+ margin: 7px 3px 17px -11px;
622+ border-radius: 6px;
623+ background: white;
624+ color: #18181c;
625+ font-size: 8px;
626+ }
627+
628+ .imsh-cast-monitor.recording {
629+ color: #f00;
630+ }
631+
632+ .imsh-cast-monitor.error {
633+ color: #ff0;
634+ }
635+
636+ /* Control-center button: your logo as a pre-rendered 18px image.
637+ GTK stretches SVG backgrounds as pixels (which makes them blurry), so logo-18.png is
638+ rendered at exactly 18px, and logo-36.png is used automatically on 2x-scaled monitors. */
639+ #custom-control-center {
640+ min-width: 18px;
641+ min-height: 18px;
642+ background-image: -gtk-scaled(url("file://${../../config/waybar/logo-18.png}"),
643+ url("file://${../../config/waybar/logo-36.png}"));
644+ background-repeat: no-repeat;
645+ background-position: center;
646+ background-size: 18px 18px;
647+ margin-right: 6px; /* gap between the logo and the end of the bar */
648+ }
649+ '';
650+ };
651+
652+ # --- Notifications --------------------------------------------------------------
653+
654+ services.dunst = {
655+ enable = true;
656+ settings =
657+ let
658+ # Near-transparent, over Hyprland's blur (the "notifications" layer rule).
659+ urgency = timeout: {
660+ inherit timeout;
661+ background = "#25252501";
662+ foreground = "#ffffff";
663+ frame_color = "#25252501";
664+ };
665+ in
666+ {
667+ global = {
668+ monitor = 0;
669+ follow = "mouse";
670+ alignment = "center";
671+ width = 360;
672+ offset = 15;
673+ origin = "top-right";
674+ padding = 12;
675+ horizontal_padding = 12;
676+ frame_width = 0;
677+ markup = "full";
678+ icon_position = "off";
679+ font = "DejaVu Sans Mono 10";
680+ stack_duplicates = true;
681+ hide_duplicate_count = true;
682+ separator_height = 0;
683+ fullscreen = "pushback";
684+ corner_radius = 15;
685+ # Left click does what the notification is for (e.g. opens the chat),
686+ # then closes it. Middle click only dismisses it, right click
687+ # dismisses all.
688+ mouse_left_click = "do_action, close_current";
689+ mouse_middle_click = "close_current";
690+ mouse_right_click = "close_all";
691+ progress_bar = true;
692+ };
693+ urgency_low = urgency 4;
694+ urgency_normal = urgency 8;
695+ urgency_critical = urgency 16;
696+ status = {
697+ category = "status";
698+ set_stack_tag = "status";
699+ history_ignore = true;
700+ timeout = 2;
701+ };
702+ };
703+ };
704+}
@@ -1,22 +1,14 @@
1-# The config files in ../../config, put where the programs read them. This
2-# replaces symlink.sh from the old dotfiles repo.
1+# The config files in ../../config that no Home Manager module writes, put where
2+# the programs read them: scripts, stylesheets and themes edited by hand, and
3+# the configs apps save themselves. The rest are set through the modules in
4+# desktop.nix, apps.nix, theming.nix and shell.nix.
35 {
46 config,
57 lib,
6- pkgs,
78 ...
89 }:
910 let
10- cfg = config.dot;
11-
12- # A file or directory of config/: linked to the checkout when liveConfig is
13- # on (edits apply at once), copied into the store otherwise.
14- link =
15- rel:
16- if cfg.liveConfig then
17- config.lib.file.mkOutOfStoreSymlink "${cfg.repoPath}/config/${rel}"
18- else
19- ../../config + "/${rel}";
11+ inherit (config.lib.dot) link;
2012
2113 # Configs the programs write themselves (MangoHud, btop, micro, apps that
2214 # register URL handlers): copied in only when missing, and never overwritten,
@@ -30,28 +22,31 @@
3022 ".config/micro/bindings.json" = "micro/bindings.json";
3123 ".config/mimeapps.list" = "mimeapps.list";
3224 };
25+
26+ # Linked whole to the checkout before Home Manager modules wrote files in them
27+ # (and kdeglobals, now set with kwriteconfig6).
28+ oldLinks = [
29+ "hypr"
30+ "waybar"
31+ "fuzzel"
32+ "kitty"
33+ "dunst"
34+ "nvim"
35+ "qt6ct"
36+ "kdeglobals"
37+ ];
3338 in
3439 {
3540 xdg.configFile = {
36- "hypr".source = link "hypr";
37- "waybar".source = link "waybar";
38- "dunst".source = link "dunst";
39- "kitty".source = link "kitty";
40- "fuzzel".source = link "fuzzel";
41- "nvim".source = link "nvim";
42- "qt6ct".source = link "qt6ct";
43- "kdeglobals".source = link "kde/kdeglobals";
41+ # Started by services.nix: the apps opened at login, the clipboard bridge.
42+ "hypr/scripts".source = link "hypr/scripts";
43+ # Waybar's flyouts (Eww) and the scripts its modules and the flyouts run.
44+ "waybar/eww".source = link "waybar/eww";
45+ "waybar/scripts".source = link "waybar/scripts";
46+ # Hyprland's mod + Return; reads favorites next to itself.
47+ "fuzzel/launch".source = link "fuzzel/launch";
48+ "fuzzel/favorites".source = link "fuzzel/favorites";
4449 "philly/theme.css".source = link "philly/theme.css";
45-
46- # Kvantum: the active theme is KvArcDark# (your customised copy of KvArcDark,
47- # made with Kvantum Manager; it uses the stock theme's artwork). RiceDark is
48- # the tweaked theme from the old repo, with the stock KvArcDark artwork
49- # copied in under its name, as kvantum/install.sh did.
50- "Kvantum/kvantum.kvconfig".source = link "kvantum/kvantum.kvconfig";
51- "Kvantum/KvArcDark#/KvArcDark#.kvconfig".source = link "kvantum/KvArcDark#.kvconfig";
52- "Kvantum/RiceDark/RiceDark.kvconfig".source = link "kvantum/RiceDark.kvconfig";
53- "Kvantum/RiceDark/RiceDark.svg".source =
54- "${pkgs.kdePackages.qtstyleplugin-kvantum}/share/Kvantum/KvArcDark/KvArcDark.svg";
5550
5651 "micro/colorschemes".source = ../../config/micro/colorschemes;
5752 };
@@ -59,6 +54,19 @@
5954 home.file.".p10k.zsh".source = link "zsh/p10k.zsh";
6055
6156 home.activation = {
57+ # The folders above were links into the checkout. Home Manager checks for
58+ # files in its way before it removes old links, so through such a link it
59+ # would find the checkout's own files (waybar/eww, hypr/scripts) and move
60+ # them aside in the repo. Remove the old links first; afterwards these are
61+ # real folders and this does nothing.
62+ dropOldLinks = lib.hm.dag.entryBefore [ "checkLinkTargets" ] (
63+ lib.concatMapStrings (name: ''
64+ if [ -L "${config.xdg.configHome}/${name}" ]; then
65+ run rm "${config.xdg.configHome}/${name}"
66+ fi
67+ '') oldLinks
68+ );
69+
6270 seedConfigs = lib.hm.dag.entryAfter [ "writeBoundary" ] (
6371 lib.concatStrings (
6472 lib.mapAttrsToList (target: rel: ''
@@ -69,27 +77,5 @@
6977 '') seeds
7078 )
7179 );
72-
73- # Firefox: user.js and userChrome.css in the default profile, which Firefox
74- # creates itself (Home Manager's own Firefox module would make a new profile
75- # and take over profiles.ini). Same as symlink.sh did.
76- firefoxProfile = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
77- ini="$HOME/.config/mozilla/firefox/profiles.ini"
78- if [ -f "$ini" ]; then
79- profile=$(${pkgs.gawk}/bin/awk -F= '
80- /^\[Install/ { install = 1; next }
81- /^\[/ { install = 0 }
82- install && $1 == "Default" { print $2; exit }
83- ' "$ini")
84- if [ -n "$profile" ]; then
85- dir="$HOME/.config/mozilla/firefox/$profile"
86- run mkdir -p "$dir/chrome"
87- run ln -sfn ${link "firefox/user.js"} "$dir/user.js"
88- run ln -sfn ${link "firefox/userChrome.css"} "$dir/chrome/userChrome.css"
89- fi
90- else
91- warnEcho "Firefox has no profile yet: start Firefox once, then run home-manager switch (or nixos-rebuild switch) again."
92- fi
93- '';
9480 };
9581 }
@@ -49,34 +49,16 @@
4949 in
5050 {
5151 systemd.user.services = {
52- waybar = graphical {
53- description = "Waybar";
54- exec = lib.getExe pkgs.waybar;
55- slice = "session-graphical.slice";
56- service.ExecReload = "${pkgs.coreutils}/bin/kill -SIGUSR2 $MAINPID";
57- };
58-
59- hypridle = graphical {
60- description = "Hyprland's idle daemon";
61- exec = lib.getExe pkgs.hypridle;
62- slice = "session-graphical.slice";
63- unit.ConditionEnvironment = "WAYLAND_DISPLAY";
64- };
65-
66- hyprpaper = graphical {
67- description = "Hyprland wallpaper daemon";
68- exec = lib.getExe pkgs.hyprpaper;
69- slice = "session-graphical.slice";
70- };
71-
72- dunst = graphical {
73- description = "Dunst notification daemon";
74- exec = lib.getExe pkgs.dunst;
75- slice = "session-graphical.slice";
76- service = {
77- Type = "dbus";
78- BusName = "org.freedesktop.Notifications";
79- };
52+ # Waybar, hypridle, hyprpaper and dunst come from their Home Manager modules
53+ # (desktop.nix); these put them with the rest of the shell.
54+ waybar.Service.Slice = "session-graphical.slice";
55+ hypridle.Service.Slice = "session-graphical.slice";
56+ hyprpaper.Service.Slice = "session-graphical.slice";
57+ dunst = {
58+ Service.Slice = "session-graphical.slice";
59+ # The module only starts dunst on the first notification (D-Bus
60+ # activation); start it with the session, as before.
61+ Install.WantedBy = [ "graphical-session.target" ];
8062 };
8163
8264 # The Waybar flyouts (control centre, notifications). Started with the
@@ -113,7 +95,7 @@
11395
11496 # The apps opened at login (Signal, Discord, Faugus, OpenDeck, WowUp); see
11597 # config/hypr/scripts/autostart-apps. It starts each one as its own unit and
116- # exits; the window placement rules for them are in hyprland.lua.
98+ # exits; the window placement rules for them are in config/hypr/startup.lua.
11799 autostart-apps = graphical {
118100 description = "Open the apps that start at login";
119101 exec = "${configDir}/hypr/scripts/autostart-apps";
@@ -1,9 +1,13 @@
11 # GTK and cursor theme, as nwg-look had set them on CachyOS (settings.ini,
2-# .gtkrc-2.0, GSettings). Qt is themed by config/qt6ct and config/kvantum.
2+# .gtkrc-2.0, GSettings), and Qt's: qt6ct with the Kvantum style.
33 #
44 # These files are now written by Nix, so change the theme here rather than in
5-# nwg-look (it can still preview themes, but can't save over these files).
6-{ pkgs, ... }:
5+# nwg-look or qt6ct (they can still preview themes, but can't save over these
6+# files).
7+{ config, pkgs, ... }:
8+let
9+ inherit (config.lib.dot) link;
10+in
711 {
812 home.pointerCursor = {
913 enable = true;
@@ -65,4 +69,66 @@
6569 cursor-size = 28;
6670 font-name = "Adwaita Sans 11";
6771 };
72+
73+ # QT_QPA_PLATFORMTHEME=qt6ct and QT_STYLE_OVERRIDE=kvantum for the session
74+ # (Hyprland's env also passes the first on, desktop.nix). qt6ct and Kvantum
75+ # themselves are installed system-wide (modules/nixos/desktop.nix).
76+ qt = {
77+ enable = true;
78+ platformTheme.name = "qt6ct";
79+ style.name = "kvantum";
80+
81+ qt6ctSettings = {
82+ Appearance = {
83+ custom_palette = false;
84+ icon_theme = "Papirus-Dark";
85+ standard_dialogs = "default";
86+ style = "kvantum";
87+ };
88+ Fonts = {
89+ fixed = ''"JetBrainsMono Nerd Font,10,-1,5,50,0,0,0,0,0"'';
90+ general = ''"Noto Sans,10,-1,5,50,0,0,0,0,0"'';
91+ };
92+ Interface = {
93+ activate_item_on_single_click = 1;
94+ buttonbox_layout = 0;
95+ cursor_flash_time = 1000;
96+ dialog_buttons_have_icons = 1;
97+ double_click_interval = 400;
98+ gui_effects = "@Invalid()";
99+ keyboard_scheme = 2;
100+ menus_have_icons = true;
101+ show_shortcuts_in_context_menus = true;
102+ stylesheets = "@Invalid()";
103+ toolbutton_style = 4;
104+ underline_shortcut = 1;
105+ wheel_scroll_lines = 3;
106+ };
107+ };
108+
109+ # The active theme is KvArcDark# (your customised copy of KvArcDark, made
110+ # with Kvantum Manager; it uses the stock theme's artwork). RiceDark is the
111+ # tweaked theme from the old repo, with the stock KvArcDark artwork copied in
112+ # under its name, as kvantum/install.sh did. Both stay linked to the
113+ # checkout.
114+ kvantum = {
115+ enable = true;
116+ settings.General.theme = "KvArcDark#";
117+ };
118+
119+ # KDE apps (Dolphin, Ark) read the style and icons from kdeglobals. Set with
120+ # kwriteconfig6, so the apps can still save their own settings there.
121+ kde.settings.kdeglobals = {
122+ General.widgetStyle = "kvantum";
123+ Icons.Theme = config.qt.qt6ctSettings.Appearance.icon_theme;
124+ KDE.widgetStyle = "kvantum";
125+ };
126+ };
127+
128+ xdg.configFile = {
129+ "Kvantum/KvArcDark#/KvArcDark#.kvconfig".source = link "kvantum/KvArcDark#.kvconfig";
130+ "Kvantum/RiceDark/RiceDark.kvconfig".source = link "kvantum/RiceDark.kvconfig";
131+ "Kvantum/RiceDark/RiceDark.svg".source =
132+ "${pkgs.kdePackages.qtstyleplugin-kvantum}/share/Kvantum/KvArcDark/KvArcDark.svg";
133+ };
68134 }
@@ -95,7 +95,7 @@
9595
9696 # --- Fonts -------------------------------------------------------------------
9797
98- # The fonts installed on CachyOS. kitty.conf asks for JetBrains Mono, which
98+ # The fonts installed on CachyOS. kitty's config asks for JetBrains Mono, which
9999 # was never installed there, so kitty has been using Noto Sans Mono; add
100100 # nerd-fonts.jetbrains-mono here to get the font the config names.
101101 fonts.packages = with pkgs; [
@@ -117,7 +117,7 @@
117117
118118 # --- Theming (system side) ------------------------------------------------------
119119
120- # Qt apps use qt6ct + Kvantum (QT_QPA_PLATFORMTHEME is set in hyprland.lua).
120+ # Qt apps use qt6ct + Kvantum (set up by Home Manager, modules/home/theming.nix).
121121 qt = {
122122 enable = true;
123123 platformTheme = "qt5ct";
@@ -55,7 +55,7 @@
5555 };
5656
5757 # libva-nvidia-driver: Firefox hardware video decoding (see the NVD_BACKEND
58- # and LIBVA_DRIVER_NAME lines in hyprland.lua).
58+ # and LIBVA_DRIVER_NAME lines in modules/home/desktop.nix).
5959 hardware.graphics.extraPackages = [ pkgs.nvidia-vaapi-driver ];
6060 })
6161 ];