dot

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

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