Kieran's opinionated (and probably slightly dumb) nix config
1{
2 outputs,
3 config,
4 lib,
5 pkgs,
6 inputs,
7 ...
8}: let
9 commonDeps = with pkgs; [coreutils gnugrep systemd];
10 # Function to simplify making waybar outputs
11 mkScript = {
12 name ? "script",
13 deps ? [],
14 script ? "",
15 }:
16 lib.getExe (pkgs.writeShellApplication {
17 inherit name;
18 text = script;
19 runtimeInputs = commonDeps ++ deps;
20 });
21 # Specialized for JSON outputs
22 mkScriptJson = {
23 name ? "script",
24 deps ? [],
25 pre ? "",
26 text ? "",
27 tooltip ? "",
28 alt ? "",
29 class ? "",
30 percentage ? "",
31 }:
32 mkScript {
33 inherit name;
34 deps = [pkgs.jq] ++ deps;
35 script = ''
36 ${pre}
37 jq -cn \
38 --arg text "${text}" \
39 --arg tooltip "${tooltip}" \
40 --arg alt "${alt}" \
41 --arg class "${class}" \
42 --arg percentage "${percentage}" \
43 '{text:$text,tooltip:$tooltip,alt:$alt,class:$class,percentage:$percentage}'
44 '';
45 };
46
47 hyprlandCfg = config.wayland.windowManager.hyprland;
48in {
49 # Let it try to start a few more times
50 systemd.user.services.waybar = {
51 Unit.StartLimitBurst = 30;
52 };
53 programs.waybar = {
54 enable = true;
55 package = pkgs.waybar.overrideAttrs (oa: {
56 mesonFlags = (oa.mesonFlags or []) ++ ["-Dexperimental=true"];
57 });
58 systemd.enable = true;
59 settings = {
60 primary = {
61 exclusive = false;
62 passthrough = false;
63 height = 46;
64 margin = "6";
65 position = "top";
66 modules-left =
67 [
68 "custom/menu"
69 "custom/currentplayer"
70 "custom/player"
71 "hyprland/workspaces"
72 "hyprland/submap"
73 ];
74
75 modules-center = [
76 "cpu"
77 "memory"
78 "disk"
79 "clock"
80 "pulseaudio"
81 "battery"
82 "idle_inhibitor"
83 # "custom/unread-mail"
84 ];
85
86 modules-right = [
87 # "custom/gammastep" TODO: currently broken for some reason
88 "network"
89 "bluetooth"
90 "tray"
91 "privacy"
92 "custom/webcam"
93 "custom/hostname"
94 ];
95
96 clock = {
97 interval = 1;
98 format = "{:%d/%m %H:%M:%S}";
99 format-alt = "{:%Y-%m-%d %H:%M:%S %z}";
100 on-click-left = "mode";
101 tooltip-format = ''
102 <big>{:%Y %B}</big>
103 <tt><small>{calendar}</small></tt>'';
104 };
105
106 cpu = {
107 format = " {usage}%";
108 };
109
110 memory = {
111 format = " {}%";
112 interval = 5;
113 };
114
115 disk = {
116 interval = 5;
117 unit = "GB";
118 format = " {percentage_used}%";
119 };
120
121 pulseaudio = {
122 format = "{icon} {volume}%";
123 format-muted = " 0%";
124 format-icons = {
125 headphone = " ";
126 headset = " ";
127 portable = " ";
128 default = [
129 " "
130 " "
131 " "
132 ];
133 };
134 on-click = lib.getExe pkgs.pavucontrol;
135 };
136
137 idle_inhibitor = {
138 format = "{icon}";
139 format-icons = {
140 activated = " ";
141 deactivated = " ";
142 };
143 tooltip-format-activated =
144 "Caffinated! device will not sleep.";
145 tooltip-format-deactivated =
146 "no caffeine :( device will sleep when not in use.";
147 };
148
149 battery = {
150 interval = 5;
151 bat = "BAT1";
152 # full-at = 94;
153 format = "{icon} {capacity}%";
154 format-icons = [ "" "" "" "" "" "" "" "" "" "" ];
155 states = {
156 battery-10 = 10;
157 battery-20 = 20;
158 battery-30 = 30;
159 battery-40 = 40;
160 battery-50 = 50;
161 battery-60 = 60;
162 battery-70 = 70;
163 battery-80 = 80;
164 battery-90 = 90;
165 battery-100 = 100;
166 };
167 # <https://github.com/Alexays/Waybar/issues/1938>
168 # the wiki lies about this, does not match
169 # /sys/class/power_supply/BAT0/status
170 format-plugged = " AC";
171 format-charging-battery-10 = " {capacity}%";
172 format-charging-battery-20 = " {capacity}%";
173 format-charging-battery-30 = " {capacity}%";
174 format-charging-battery-40 = " {capacity}%";
175 format-charging-battery-50 = " {capacity}%";
176 format-charging-battery-60 = " {capacity}%";
177 format-charging-battery-70 = " {capacity}%";
178 format-charging-battery-80 = " {capacity}%";
179 format-charging-battery-90 = " {capacity}%";
180 format-charging-battery-100 = " {capacity}%";
181 };
182
183 "hyprland/workspaces" = {
184 format = "{icon} {windows}";
185 window-rewrite-default = " ";
186 window-rewrite-seperator = "";
187 window-rewrite = {
188 "title<.*github.*>" = "";
189 "title<.*youtube.*>" = "";
190 "title<*Gmail*>" = "";
191 "class<firefox>" = "";
192 "class<com.obsproject.Studio>" = "";
193 "title<OBS*>" = "";
194 "alacritty" = "";
195 "code" = "";
196 "slack" = "";
197 "initialtitle<Spotify*>" = "";
198 "class<org.gnome.Nautilus>" = "";
199 "class<kicad>" = "";
200 };
201 };
202
203 network = {
204 interval = 3;
205 format-wifi = "{icon} {essid}";
206 format-icons = [ "" "" "" "" ];
207 format-ethernet = " Connected";
208 format-disconnected = " ";
209 tooltip-format = ''
210 {ifname}
211 {ipaddr}/{cidr}
212 Up: {bandwidthUpBits}
213 Down: {bandwidthDownBits}'';
214 on-click = mkScript {
215 deps = [pkgs.wpa_supplicant pkgs.notify-desktop];
216 script = ''wpa_cli reconnect; notify-desktop "reconnecting to wifi" -t 1200'';
217 };
218 };
219
220 bluetooth = {
221 format-on = "";
222 format-off = "";
223 format-disabled = "";
224 format-connected = " {num_connections}";
225 format-connected-battery = " {device_alias} ({device_battery_percentage}%) ({num_connections})";
226 on-click = "overskride";
227 };
228
229 "custom/menu" = {
230 interval = 1;
231 return-type = "json";
232 exec = mkScriptJson {
233 deps = lib.optional hyprlandCfg.enable hyprlandCfg.package;
234 text = " ";
235 tooltip = ''$(grep PRETTY_NAME /etc/os-release | cut -d '"' -f2)'';
236 class = let
237 isFullScreen =
238 if hyprlandCfg.enable
239 then "hyprctl activewindow -j | jq -e '.fullscreen' &>/dev/null"
240 else "false";
241 in "$(if ${isFullScreen}; then echo fullscreen; fi)";
242 };
243 };
244
245 "custom/hostname" = {
246 exec = mkScript {script = ''echo "$USER@$HOSTNAME"'';};
247 on-click = mkScript {script = "systemctl --user restart waybar";};
248 };
249
250 "custom/unread-mail" = {
251 interval = 5;
252 return-type = "json";
253 exec = mkScriptJson {
254 deps = [pkgs.findutils pkgs.procps];
255 pre = ''
256 count=$(find ~/Mail/*/Inbox/new -type f | wc -l)
257 if pgrep mbsync &>/dev/null; then
258 status="syncing"
259 else
260 if [ "$count" == "0" ]; then
261 status="read"
262 else
263 status="unread"
264 fi
265 fi
266 '';
267 text = "$count";
268 alt = "$status";
269 };
270 format = "{icon} ({})";
271 format-icons = {
272 "read" = "";
273 "unread" = "";
274 "syncing" = "";
275 };
276 };
277
278 "custom/currentplayer" = {
279 interval = 2;
280 return-type = "json";
281 exec = mkScriptJson {
282 deps = [pkgs.playerctl];
283 pre = ''
284 player="$(playerctl status -f "{{playerName}}" 2>/dev/null || echo "No player active" | cut -d '.' -f1)"
285 count="$(playerctl -l 2>/dev/null | wc -l)"
286 if ((count > 1)); then
287 more=" +$((count - 1))"
288 else
289 more=""
290 fi
291 '';
292 alt = "$player";
293 tooltip = "$player ($count available)";
294 text = "$more";
295 };
296 format = "{icon}{}";
297 format-icons = {
298 "No player active" = " ";
299 "Celluloid" = " ";
300 "spotify" = " ";
301 "ncspot" = " ";
302 "qutebrowser" = " ";
303 "firefox" = " ";
304 "discord" = " ";
305 "sublimemusic" = " ";
306 "kdeconnect" = " ";
307 "chromium" = " ";
308 };
309 };
310
311 privacy = {
312 "icon-spacing" = 0;
313 "icon-size" = 18;
314 "transition-duration" = 250;
315 modules = [
316 {
317 type = "screenshare";
318 tooltip = true;
319 "tooltip-icon-size" = 24;
320 }
321 {
322 type = "audio-out";
323 tooltip = true;
324 "tooltip-icon-size" = 24;
325 }
326 {
327 type = "audio-in";
328 tooltip = true;
329 "tooltip-icon-size" = 24;
330 }
331 ];
332 };
333
334 "custom/webcam" = {
335 return-type = "json";
336 interval = 2;
337 exec = mkScript {
338 deps = [pkgs.jq pkgs.psmisc];
339 script = ''
340 # get programs using the video0 endpoint
341 ps -eo user,pid,cmd -q "$(fuser /dev/video0 2>/dev/null | xargs)" |\
342 # omit the column headings and the first line which is wireplumber
343 sed -n "1,2!p" |\
344 # just get the pid and program columns
345 awk '{print $2 " " $3}' |\
346 # filter out the program path
347 awk -F "/" '{print "{\"tooltip\": \"" $1 " " $NF "\"}"}' |\
348 jq -s 'if length > 0 then {text: " ", tooltip: (map(.tooltip) | join("\r"))} else {text: "", tooltip: ""} end' |\
349 jq --unbuffered --compact-output
350 '';
351 };
352 };
353
354 "custom/player" = {
355 exec-if = mkScript {
356 deps = [pkgs.playerctl];
357 script = "playerctl status 2>/dev/null";
358 };
359 exec = let
360 format = ''{"text": "{{title}} - {{artist}}", "alt": "{{status}}", "tooltip": "{{title}} - {{artist}} ({{album}})"}'';
361 in
362 mkScript {
363 deps = [pkgs.playerctl];
364 script = "playerctl metadata --format '${format}' 2>/dev/null";
365 };
366 return-type = "json";
367 interval = 2;
368 max-length = 30;
369 format = "{icon} {}";
370 format-icons = {
371 "Playing" = "";
372 "Paused" = " ";
373 "Stopped" = "";
374 };
375 on-click = mkScript {
376 deps = [pkgs.playerctl];
377 script = "playerctl play-pause";
378 };
379 };
380 };
381 };
382 # Cheatsheet:
383 # x -> all sides
384 # x y -> vertical, horizontal
385 # x y z -> top, horizontal, bottom
386 # w x y z -> top, right, bottom, left
387 style = let
388 inherit (inputs.nix-colors.lib.conversions) hexToRGBString;
389 inherit (config.colorscheme) colors;
390 toRGBA = color: opacity: "rgba(${hexToRGBString "," (lib.removePrefix "#" color)},${opacity})";
391 in
392 /*
393 css
394 */
395 ''
396 * {
397 font-family: Fira Sans, FiraCode Nerd Font;
398 font-size: 12pt;
399 padding: 0;
400 margin: 0 0.4em;
401 }
402
403 window#waybar {
404 padding: 0;
405 border-radius: 0.5em;
406 background-color: shade(@surface0, 0.7);
407 color: @surface2
408 }
409 .modules-left {
410 margin-left: -0.65em;
411 }
412 .modules-right {
413 margin-right: -0.65em;
414 }
415
416 #workspaces button {
417 background-color: @surface0;
418 color: @surface2;
419 padding-left: 0.2em;
420 padding-right: 0.2em;
421 margin-left: 0.25em;
422 margin-right: 0.25em;
423 margin-top: 0.4em;
424 margin-bottom: 0.4em;
425 }
426 #workspaces button.hidden {
427 background-color: @surface0;
428 color: @surface2;
429 }
430 #workspaces button.focused,
431 #workspaces button.active {
432 background-color: shade(@blue, 0.7);
433 color: @green;
434 }
435
436 #workspaces button:hover {
437 background-color: @surface3;
438 color: @surface1;
439 }
440
441 #privacy-item {
442 margin-left: 0.1em;
443 margin-right: 0.1em;
444 }
445
446 #clock {
447 padding-right: 1em;
448 padding-left: 1em;
449 border-radius: 0.5em;
450 }
451
452 #custom-menu {
453 background-color: @surface3;
454 color: @blue;
455 padding-right: 1.5em;
456 padding-left: 1em;
457 margin-right: 0;
458 border-radius: 0.5em;
459 }
460 #custom-menu.fullscreen {
461 background-color: @blue;
462 color: @green;
463 }
464 #custom-hostname {
465 background-color: @surface3;
466 color: @blue;
467 padding-right: 1em;
468 padding-left: 1em;
469 margin-left: 0;
470 border-radius: 0.5em;
471 }
472 #custom-currentplayer {
473 padding-right: 0;
474 }
475 #custom-gpu, #cpu, #memory {
476 margin-left: 0.05em;
477 margin-right: 0.55em;
478 }
479 '';
480 };
481}