dot

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

274 lines14179 bytes
1;; Flyout panels opened from the Waybar buttons (scripts/toggle.sh): the control
2;; center and the notification list. `cc-closer` is an invisible click-catcher (click
3;; outside = close), shared by both panels. All windows take a `monitor` argument (a
4;; number), which scripts/toggle.sh fills in with the monitor you clicked on.
5
6;; ---------------------------------------------------------------------------
7;; State
8;; ---------------------------------------------------------------------------
9
10(defvar confirm "none") ;; none | logout | reboot | poweroff
11
12(deflisten audio
13 :initial '{"vol":0,"muted":false,"mic":0,"mic_muted":false}'
14 "python3 $HOME/.config/waybar/eww/scripts/audio.py")
15
16(defpoll net
17 :interval "3s"
18 :initial '{"available":false,"on":false,"connected":false,"icon":"","label":"Unavailable"}'
19 "bash $HOME/.config/waybar/eww/scripts/network.sh")
20
21(defpoll mouse
22 :interval "30s"
23 :initial '{"text":"","percentage":0,"class":"","tooltip":"","label":"Not connected","connected":false}'
24 "bash $HOME/.config/waybar/scripts/mouse-battery.sh")
25
26(defpoll bt
27 :interval "2s"
28 :initial '{"available":false,"on":false,"device":"","label":"Unavailable"}'
29 "bash $HOME/.config/waybar/eww/scripts/bluetooth.sh")
30
31(defpoll disk
32 :interval "30s"
33 :initial "0"
34 "df --output=pcent / | tail -1 | tr -dc '0-9'")
35
36;; "26.11 · 15 Oct" (release · nixpkgs date) of this desktop and of the server.
37(defpoll nixos
38 :interval "10m"
39 :initial ""
40 "bash $HOME/.config/waybar/eww/scripts/nixos-version.sh")
41
42(defpoll nixos_server
43 :interval "10m"
44 :initial ""
45 "bash $HOME/.config/waybar/eww/scripts/nixos-version.sh granstrm")
46
47;; Last notifications from dunst's history. Only polled while the panel is open;
48;; scripts/toggle.sh fills it once right before opening.
49(defvar notifs_open false)
50;; Clear animation (scripts/clear-notifications.sh): entries first slide out one after
51;; another (notifs_clearing), then the emptied list folds up (notifs_folded).
52(defvar notifs_clearing false)
53(defvar notifs_folded false)
54(defpoll notifs
55 :interval "2s"
56 :run-while notifs_open
57 :initial "[]"
58 "python3 $HOME/.config/waybar/eww/scripts/notifications.py")
59
60;; ---------------------------------------------------------------------------
61;; Windows
62;; ---------------------------------------------------------------------------
63
64(defwindow cc-closer [monitor]
65 :monitor monitor
66 :geometry (geometry :x "0px" :y "0px" :width "100%" :height "100%" :anchor "top left")
67 :stacking "overlay"
68 :namespace "eww-cc-closer"
69 (eventbox :class "closer"
70 :timeout "2s"
71 :onclick "bash $HOME/.config/waybar/eww/scripts/toggle.sh close"))
72
73(defwindow control-center [monitor]
74 :monitor monitor
75 :geometry (geometry :x "15px" :y "8px" :width "360px" :anchor "top right")
76 :stacking "overlay"
77 :namespace "eww-control-center"
78 (control-center))
79
80;; Same spot and namespace as the control center, so Hyprland's blur rule applies.
81(defwindow notifications [monitor]
82 :monitor monitor
83 :geometry (geometry :x "15px" :y "8px" :width "360px" :anchor "top right")
84 :stacking "overlay"
85 :namespace "eww-control-center"
86 (notifications))
87
88;; ---------------------------------------------------------------------------
89;; Layout
90;; ---------------------------------------------------------------------------
91
92(defwidget control-center []
93 (box :class "cc" :orientation "v" :space-evenly false :spacing 10
94 (box :orientation "h" :space-evenly true :spacing 10
95 (split-tile :icon {net.icon}
96 :title "Network"
97 :sub {net.label}
98 :active {net.available && net.connected}
99 :ontoggle "bash $HOME/.config/waybar/eww/scripts/net-toggle.sh ${net.on ? 'off' : 'on'}"
100 :onopen "${EWW_CMD} close control-center cc-closer; setsid -f nm-connection-editor")
101 (split-tile :icon {bt.on ? "󰂯" : "󰂲"}
102 :title "Bluetooth"
103 :sub {bt.label}
104 :active {bt.available && bt.on}
105 :ontoggle "bash $HOME/.config/waybar/eww/scripts/bt-toggle.sh ${bt.on ? 'off' : 'on'}"
106 :onopen "${EWW_CMD} close control-center cc-closer; setsid -f blueman-manager"))
107 (sound)
108 (stats)
109 (power)
110 (update)))
111
112;; ---------------------------------------------------------------------------
113;; Building blocks
114;; ---------------------------------------------------------------------------
115
116;; A tile in two halves: the round icon switches it on and off, the rest opens its
117;; settings. The halves sit side by side rather than one inside the other, because
118;; a click on an inner eventbox would reach the outer one too.
119(defwidget split-tile [icon title sub active ontoggle onopen]
120 (box :class "tile split" :orientation "h" :space-evenly false
121 (eventbox :class "toggle-wrap" :cursor "pointer" :timeout "2s" :onclick ontoggle
122 (box :class "toggle"
123 (box :class "icon-circle ${active ? 'on' : ''}" :space-evenly true
124 (label :text icon))))
125 (eventbox :class "open-wrap" :cursor "pointer" :timeout "2s" :hexpand true :onclick onopen
126 (box :class "open" :orientation "h" :space-evenly false
127 (box :orientation "v" :valign "center" :halign "start" :hexpand true :space-evenly false
128 (label :class "tile-title" :halign "start" :limit-width 12 :text title)
129 (label :class "tile-sub" :halign "start" :limit-width 15 :text sub))
130 (label :class "chevron" :valign "center" :text "󰅂")))))
131
132(defwidget slider-row [icon muted value onmute onchange]
133 (box :class "slider-row" :orientation "h" :space-evenly false :spacing 10
134 (eventbox :cursor "pointer" :timeout "1s" :onclick onmute
135 (label :class "slider-icon ${muted ? 'muted' : ''}" :text icon))
136 (scale :class "slider" :hexpand true :min 0 :max 100 :round-digits 0
137 :timeout "1s" :value value :onchange onchange)
138 (label :class "slider-val" :text "${value}%")))
139
140;; `text` replaces the percentage in the middle; `hot` (red) defaults to above 85%.
141(defwidget ring [value name ?text ?hot]
142 (box :class "ring-wrap" :orientation "v" :space-evenly false :spacing 6 :hexpand true
143 (overlay :halign "center"
144 (circular-progress :class "ring ${(hot ?: value > 85) ? 'hot' : ''}"
145 :value value :thickness 6 :start-at 75 :clockwise true
146 :width 60 :height 60)
147 (label :class "ring-value" :text {text ?: "${round(value, 0)}%"}))
148 (label :class "ring-name" :text name)))
149
150(defwidget power-btn [icon label onclick]
151 ;; :height is a hard GTK size request (unlike CSS padding, which the box/eventbox
152 ;; layout here wasn't actually growing from -- the row just stayed shrink-wrapped
153 ;; to the icon+label no matter how much padding was added). This is what actually
154 ;; makes the hover rectangle taller.
155 (eventbox :class "power-btn" :cursor "pointer" :timeout "3s" :onclick onclick :height 62
156 (box :orientation "v" :space-evenly false :spacing 4 :valign "center" :halign "center"
157 (label :class "power-icon" :text icon)
158 (label :class "power-label" :text label))))
159
160;; ---------------------------------------------------------------------------
161;; Cards
162;; ---------------------------------------------------------------------------
163
164(defwidget sound []
165 (box :class "card" :orientation "v" :space-evenly false :spacing 10
166 (label :class "card-title" :halign "start" :text "Sound")
167 (slider-row :icon {audio.muted ? "󰝟" : "󰕾"}
168 :muted {audio.muted}
169 :value {audio.vol}
170 :onmute "pactl set-sink-mute @DEFAULT_SINK@ toggle"
171 :onchange "pactl set-sink-volume @DEFAULT_SINK@ {}%")
172 (slider-row :icon {audio.mic_muted ? "󰍭" : "󰍬"}
173 :muted {audio.mic_muted}
174 :value {audio.mic}
175 :onmute "pactl set-source-mute @DEFAULT_SOURCE@ toggle"
176 :onchange "pactl set-source-volume @DEFAULT_SOURCE@ {}%")))
177
178(defwidget stats []
179 (box :class "card" :orientation "h" :space-evenly true
180 (ring :value {EWW_CPU.avg} :name "CPU")
181 (ring :value {EWW_RAM.used_mem_perc} :name "RAM")
182 (ring :value disk :name "Disk")
183 ;; The Superlight's battery; red when low rather than when full.
184 (ring :value {mouse.connected ? mouse.percentage : 0}
185 :text {mouse.connected ? "${mouse.percentage}%" : "–"}
186 :hot {mouse.class == "warning" || mouse.class == "critical"}
187 :name {mouse.class == "charging" ? "Charging" : "Mouse"})))
188
189(defwidget power []
190 (box :class "card" :orientation "v" :space-evenly false :spacing 6
191 (box :orientation "h" :space-evenly true
192 (power-btn :icon "󰌾" :label "Lock"
193 :onclick "${EWW_CMD} close control-center cc-closer; pidof hyprlock || setsid -f hyprlock")
194 (power-btn :icon "󰒲" :label "Sleep"
195 :onclick "${EWW_CMD} close control-center cc-closer; systemctl suspend")
196 (power-btn :icon "󰍃" :label "Log out"
197 :onclick "${EWW_CMD} update confirm=logout")
198 (power-btn :icon "󰜉" :label "Restart"
199 :onclick "${EWW_CMD} update confirm=reboot")
200 (power-btn :icon "󰐥" :label "Shut down"
201 :onclick "${EWW_CMD} update confirm=poweroff"))
202 (revealer :transition "slidedown" :duration "150ms" :reveal {confirm != "none"}
203 (box :class "confirm" :orientation "h" :space-evenly false :spacing 8
204 (label :hexpand true :halign "start"
205 :text {confirm == "reboot" ? "Restart now?" : (confirm == "poweroff" ? "Shut down now?" : "Log out now?")})
206 ;; Padding/background live on the inner box: GTK ignores CSS padding on an
207 ;; eventbox, so styling the eventbox itself left the pills shrink-wrapped.
208 (eventbox :class "btn-wrap" :cursor "pointer" :onclick "${EWW_CMD} update confirm=none"
209 (box :class "btn"
210 (label :text "Cancel")))
211 (eventbox :class "btn-wrap" :cursor "pointer" :timeout "3s"
212 :onclick "${EWW_CMD} update confirm=none; ${EWW_CMD} close control-center cc-closer; ${confirm == 'reboot' ? 'systemctl reboot' : (confirm == 'poweroff' ? 'systemctl poweroff' : 'hyprctl dispatch exit')}"
213 (box :class "btn danger"
214 (label :text "Confirm")))))))
215
216;; Update NixOS, in two halves like the split tiles: this desktop (tools/nixos-update,
217;; reboots when done) and the server (tools/server-update, switches it over SSH). Each
218;; runs in a kitty window and asks before it installs anything.
219(defwidget update []
220 (box :class "tile split" :orientation "h" :space-evenly true
221 (update-half :icon "󰍹" :title "Desktop" :sub nixos
222 :tooltip "Update NixOS on this desktop"
223 :command "nixos-update")
224 (update-half :icon "󰒋" :title "Server" :sub nixos_server
225 :tooltip "Update NixOS on the server"
226 :command "server-update")))
227
228(defwidget update-half [icon title sub tooltip command]
229 (eventbox :class "open-wrap" :cursor "pointer" :timeout "3s" :hexpand true :tooltip tooltip
230 :onclick "${EWW_CMD} close control-center cc-closer; setsid -f kitty --title '${tooltip}' ${command}"
231 (box :class "open" :orientation "h" :space-evenly false :spacing 10
232 (box :class "icon-circle" :space-evenly true
233 (label :text icon))
234 (box :orientation "v" :valign "center" :halign "start" :hexpand true :space-evenly false
235 (label :class "tile-title" :halign "start" :text title)
236 (label :class "tile-sub" :halign "start" :limit-width 15 :text sub)))))
237
238;; ---------------------------------------------------------------------------
239;; Notifications panel
240;; ---------------------------------------------------------------------------
241
242(defwidget notifications []
243 ;; One panel, no inner card: the header sits straight on the panel and every
244 ;; notification is its own card. Spacing comes from CSS margins rather than
245 ;; :spacing, because the two revealers stay in the layout even when folded shut.
246 (box :class "cc" :orientation "v" :space-evenly false
247 ;; Fixed height, so the header doesn't shrink when the Clear button hides.
248 (box :class "notif-header" :orientation "h" :space-evenly false :height 28
249 (label :class "card-title" :hexpand true :halign "start" :valign "center" :text "Notifications")
250 (eventbox :class "btn-wrap" :cursor "pointer" :timeout "2s" :valign "center"
251 :visible {arraylength(notifs) > 0 && !notifs_clearing}
252 :onclick "bash $HOME/.config/waybar/eww/scripts/clear-notifications.sh &"
253 (box :class "btn"
254 (label :text "Clear"))))
255 ;; Slides in while the list folds up, so the panel goes straight to its empty size.
256 (revealer :transition "slidedown" :duration "220ms"
257 :reveal {arraylength(notifs) == 0 || notifs_folded}
258 (label :class "notif-empty" :halign "start" :text "No notifications"))
259 (revealer :transition "slideup" :duration "220ms" :reveal {!notifs_folded}
260 (box :orientation "v" :space-evenly false
261 (for n in notifs
262 ;; Click: jump to the app that sent it (scripts/open-notification.py). Only the
263 ;; numeric id is passed, never the notification's own text.
264 (eventbox :class "notif-wrap" :cursor "pointer" :timeout "3s"
265 :onclick "python3 $HOME/.config/waybar/eww/scripts/open-notification.py ${n.id} &"
266 ;; `d<n>` delays each entry's slide-out a little more than the one above it
267 (box :class "notif d${n.i} ${notifs_clearing ? 'out' : ''}"
268 :orientation "v" :space-evenly false :spacing 2
269 (box :orientation "h" :space-evenly false
270 (label :class "notif-meta" :hexpand true :halign "start" :limit-width 30 :text {n.app})
271 (label :class "notif-meta" :text {n.time}))
272 (label :class "notif-summary" :halign "start" :xalign 0 :wrap true :text {n.summary})
273 (label :class "notif-body" :halign "start" :xalign 0 :wrap true
274 :visible {n.body != ""} :text {n.body}))))))))