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 "alacritty" = "";
193 "code" = "";
194 "slack" = "";
195 "initialtitle<Spotify*>" = "";
196 "class<org.gnome.Nautilus>" = "";
197 };
198 };
199
200 network = {
201 interval = 3;
202 format-wifi = " {essid}";
203 format-ethernet = " Connected";
204 format-disconnected = " ";
205 tooltip-format = ''
206 {ifname}
207 {ipaddr}/{cidr}
208 Up: {bandwidthUpBits}
209 Down: {bandwidthDownBits}'';
210 on-click = mkScript {
211 deps = [pkgs.wpa_supplicant pkgs.notify-desktop];
212 script = ''wpa_cli reconnect; notify-desktop "reconnecting to wifi" -t 1200'';
213 };
214 };
215
216 bluetooth = {
217 format-on = "";
218 format-off = "";
219 format-disabled = "";
220 format-connected = " {num_connections}";
221 format-connected-battery = " {device_alias} ({device_battery_percentage}%) ({num_connections})";
222 on-click = "overskride";
223 };
224
225 "custom/menu" = {
226 interval = 1;
227 return-type = "json";
228 exec = mkScriptJson {
229 deps = lib.optional hyprlandCfg.enable hyprlandCfg.package;
230 text = " ";
231 tooltip = ''$(grep PRETTY_NAME /etc/os-release | cut -d '"' -f2)'';
232 class = let
233 isFullScreen =
234 if hyprlandCfg.enable
235 then "hyprctl activewindow -j | jq -e '.fullscreen' &>/dev/null"
236 else "false";
237 in "$(if ${isFullScreen}; then echo fullscreen; fi)";
238 };
239 };
240
241 "custom/hostname" = {
242 exec = mkScript {script = ''echo "$USER@$HOSTNAME"'';};
243 on-click = mkScript {script = "systemctl --user restart waybar";};
244 };
245
246 "custom/unread-mail" = {
247 interval = 5;
248 return-type = "json";
249 exec = mkScriptJson {
250 deps = [pkgs.findutils pkgs.procps];
251 pre = ''
252 count=$(find ~/Mail/*/Inbox/new -type f | wc -l)
253 if pgrep mbsync &>/dev/null; then
254 status="syncing"
255 else
256 if [ "$count" == "0" ]; then
257 status="read"
258 else
259 status="unread"
260 fi
261 fi
262 '';
263 text = "$count";
264 alt = "$status";
265 };
266 format = "{icon} ({})";
267 format-icons = {
268 "read" = "";
269 "unread" = "";
270 "syncing" = "";
271 };
272 };
273
274 "custom/currentplayer" = {
275 interval = 2;
276 return-type = "json";
277 exec = mkScriptJson {
278 deps = [pkgs.playerctl];
279 pre = ''
280 player="$(playerctl status -f "{{playerName}}" 2>/dev/null || echo "No player active" | cut -d '.' -f1)"
281 count="$(playerctl -l 2>/dev/null | wc -l)"
282 if ((count > 1)); then
283 more=" +$((count - 1))"
284 else
285 more=""
286 fi
287 '';
288 alt = "$player";
289 tooltip = "$player ($count available)";
290 text = "$more";
291 };
292 format = "{icon}{}";
293 format-icons = {
294 "No player active" = " ";
295 "Celluloid" = " ";
296 "spotify" = " ";
297 "ncspot" = " ";
298 "qutebrowser" = " ";
299 "firefox" = " ";
300 "discord" = " ";
301 "sublimemusic" = " ";
302 "kdeconnect" = " ";
303 "chromium" = " ";
304 };
305 };
306
307 privacy = {
308 "icon-spacing" = 0;
309 "icon-size" = 18;
310 "transition-duration" = 250;
311 modules = [
312 {
313 type = "screenshare";
314 tooltip = true;
315 "tooltip-icon-size" = 24;
316 }
317 {
318 type = "audio-out";
319 tooltip = true;
320 "tooltip-icon-size" = 24;
321 }
322 {
323 type = "audio-in";
324 tooltip = true;
325 "tooltip-icon-size" = 24;
326 }
327 ];
328 };
329
330 "custom/webcam" = {
331 return-type = "json";
332 interval = 2;
333 exec = mkScript {
334 deps = [pkgs.jq pkgs.psmisc];
335 script = ''
336 # get programs using the video0 endpoint
337 ps -eo user,pid,cmd -q "$(fuser /dev/video0 2>/dev/null | xargs)" |\
338 # omit the column headings and the first line which is wireplumber
339 sed -n "1,2!p" |\
340 # just get the pid and program columns
341 awk '{print $2 " " $3}' |\
342 # filter out the program path
343 awk -F "/" '{print "{\"tooltip\": \"" $1 " " $NF "\"}"}' |\
344 jq -s 'if length > 0 then {text: " ", tooltip: (map(.tooltip) | join("\r"))} else {text: "", tooltip: ""} end' |\
345 jq --unbuffered --compact-output
346 '';
347 };
348 };
349
350 "custom/player" = {
351 exec-if = mkScript {
352 deps = [pkgs.playerctl];
353 script = "playerctl status 2>/dev/null";
354 };
355 exec = let
356 format = ''{"text": "{{title}} - {{artist}}", "alt": "{{status}}", "tooltip": "{{title}} - {{artist}} ({{album}})"}'';
357 in
358 mkScript {
359 deps = [pkgs.playerctl];
360 script = "playerctl metadata --format '${format}' 2>/dev/null";
361 };
362 return-type = "json";
363 interval = 2;
364 max-length = 30;
365 format = "{icon} {}";
366 format-icons = {
367 "Playing" = "";
368 "Paused" = " ";
369 "Stopped" = "";
370 };
371 on-click = mkScript {
372 deps = [pkgs.playerctl];
373 script = "playerctl play-pause";
374 };
375 };
376 };
377 };
378 # Cheatsheet:
379 # x -> all sides
380 # x y -> vertical, horizontal
381 # x y z -> top, horizontal, bottom
382 # w x y z -> top, right, bottom, left
383 style = let
384 inherit (inputs.nix-colors.lib.conversions) hexToRGBString;
385 inherit (config.colorscheme) colors;
386 toRGBA = color: opacity: "rgba(${hexToRGBString "," (lib.removePrefix "#" color)},${opacity})";
387 in
388 /*
389 css
390 */
391 ''
392 * {
393 font-family: Fira Sans, FiraCode Nerd Font;
394 font-size: 12pt;
395 padding: 0;
396 margin: 0 0.4em;
397 }
398
399 window#waybar {
400 padding: 0;
401 border-radius: 0.5em;
402 background-color: shade(@surface0, 0.7);
403 color: @surface2
404 }
405 .modules-left {
406 margin-left: -0.65em;
407 }
408 .modules-right {
409 margin-right: -0.65em;
410 }
411
412 #workspaces button {
413 background-color: @surface0;
414 color: @surface2;
415 padding-left: 0.2em;
416 padding-right: 0.2em;
417 margin-left: 0.25em;
418 margin-right: 0.25em;
419 margin-top: 0.4em;
420 margin-bottom: 0.4em;
421 }
422 #workspaces button.hidden {
423 background-color: @surface0;
424 color: @surface2;
425 }
426 #workspaces button.focused,
427 #workspaces button.active {
428 background-color: shade(@blue, 0.7);
429 color: @green;
430 }
431
432 #workspaces button:hover {
433 background-color: @surface3;
434 color: @surface1;
435 }
436
437 #privacy-item {
438 margin-left: 0.1em;
439 margin-right: 0.1em;
440 }
441
442 #clock {
443 padding-right: 1em;
444 padding-left: 1em;
445 border-radius: 0.5em;
446 }
447
448 #custom-menu {
449 background-color: @surface3;
450 color: @blue;
451 padding-right: 1.5em;
452 padding-left: 1em;
453 margin-right: 0;
454 border-radius: 0.5em;
455 }
456 #custom-menu.fullscreen {
457 background-color: @blue;
458 color: @green;
459 }
460 #custom-hostname {
461 background-color: @surface3;
462 color: @blue;
463 padding-right: 1em;
464 padding-left: 1em;
465 margin-left: 0;
466 border-radius: 0.5em;
467 }
468 #custom-currentplayer {
469 padding-right: 0;
470 }
471 #custom-gpu, #cpu, #memory {
472 margin-left: 0.05em;
473 margin-right: 0.55em;
474 }
475 '';
476 };
477}