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}