# Waybar — two-tier theming: # # 1. Default: structure, fonts, geometry AND palette baked from # theme-state.json (colors as GTK named colors, @define-color). # # 2. Whole-swap: themes with their own visual identity ship # //waybar.css (and optionally waybar.jsonc) which # replace the generated style/layout entirely. Probed at eval time — # pure, it's flake source. { config, lib, pkgs, ... }: let t = config.nomarchy.theme; # Show the active-keyboard-layout indicator only when more than one layout # is in play — multiple session layouts (comma-separated) or per-device # overrides (nomarchy.keyboard.devices) — so single-layout bars stay clean. showLanguage = lib.hasInfix "," config.nomarchy.keyboard.layout || config.nomarchy.keyboard.layouts != [ ] || config.nomarchy.keyboard.devices != { }; # Power-profile indicator (power-profiles-daemon). Both scripts self- # gate: they exit silently unless this is a laptop (a battery is # present) running PPD, so the module hides itself on desktops and # under the TLP backend — no system→home coupling. Speedometer glyphs # (slow/medium/fast) map to power-saver/balanced/performance. # # Named writeShellScriptBins (put on PATH via home.packages below) rather # than bare writeShellScript store paths, so the whole-swap themes' static # waybar.jsonc can exec them by name too — same as the swaync bell. powerProfileStatus = pkgs.writeShellScriptBin "nomarchy-powerprofile-status" '' case "$(ls /sys/class/power_supply/ 2>/dev/null)" in *BAT*) ;; *) exit 0 ;; esac command -v powerprofilesctl >/dev/null 2>&1 || exit 0 prof=$(powerprofilesctl get 2>/dev/null) || exit 0 case "$prof" in power-saver) icon="󰾆" ;; performance) icon="󰓅" ;; *) icon="󰾅" ;; esac printf '{"text":"%s","tooltip":"Power profile: %s","class":"%s"}\n' "$icon" "$prof" "$prof" ''; # Cycle to the next profile this machine actually offers (wraps around). powerProfileCycle = pkgs.writeShellScriptBin "nomarchy-powerprofile-cycle" '' command -v powerprofilesctl >/dev/null 2>&1 || exit 0 cur=$(powerprofilesctl get 2>/dev/null) || exit 0 avail=$(powerprofilesctl list 2>/dev/null | sed -nE 's/^[* ] ([a-z-]+):$/\1/p') next=$(printf '%s\n' "$avail" | awk -v cur="$cur" ' { a[NR] = $0 } END { for (i = 1; i <= NR; i++) if (a[i] == cur) { print a[i % NR + 1]; f = 1 } if (!f && NR) print a[1] }') [ -n "$next" ] && powerprofilesctl set "$next" ''; # Per-theme override probe. assetDir = config.nomarchy.themesDir + "/${t.slug}"; styleOverride = assetDir + "/waybar.css"; configOverride = assetDir + "/waybar.jsonc"; hasStyleOverride = builtins.pathExists styleOverride; hasConfigOverride = builtins.pathExists configOverride; # The palette as GTK named colors, straight from the JSON. colorDefs = lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "@define-color ${name} ${value};") t.colors); generatedSettings = { layer = "top"; position = "top"; height = 34; margin-top = t.ui.gapsOut; margin-left = t.ui.gapsOut; margin-right = t.ui.gapsOut; spacing = 8; # waybar re-reads style.css when it changes on disk, so a # home-manager switch restyles the running bar without a restart. reload_style_on_change = true; modules-left = [ "hyprland/workspaces" "hyprland/window" ]; modules-center = [ "clock" ]; modules-right = [ "tray" "pulseaudio" "cpu" "memory" "custom/powerprofile" ] ++ lib.optional showLanguage "hyprland/language" ++ [ "battery" "custom/notification" ]; "hyprland/workspaces" = { format = "{icon}"; on-click = "activate"; }; "hyprland/window" = { max-length = 48; separate-outputs = true; }; clock = { format = "{:%H:%M}"; format-alt = "{:%A %d %B %Y}"; tooltip-format = "{calendar}"; }; # Active keyboard layout (per focused device) — only placed in # modules-right when showLanguage (see above). "hyprland/language" = { format = "󰌌 {short}"; tooltip = false; }; pulseaudio = { format = "{icon} {volume}%"; format-muted = "󰝟"; format-icons.default = [ "󰕿" "󰖀" "󰕾" ]; on-click = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; }; # No network module: nm-applet lives in the tray (the GUI path), so the # bar's wifi/ethernet indicator would just duplicate it. cpu.format = "󰍛 {usage}%"; memory.format = "󰾆 {percentage}%"; battery = { states = { warning = 25; critical = 10; }; format = "{icon} {capacity}%"; format-charging = "󰂄 {capacity}%"; format-icons = [ "󰁺" "󰁼" "󰁾" "󰂀" "󰁹" ]; }; "custom/powerprofile" = { exec = "nomarchy-powerprofile-status"; return-type = "json"; interval = 5; on-click = "nomarchy-powerprofile-cycle"; }; # swaync notification bell + Do-Not-Disturb state. `-swb` streams JSON # (text/tooltip/class) on every change, so it tracks count and DND with # no polling. Left-click toggles the panel; right-click toggles DND. # The `dnd-*` classes (bell-off glyph) are styled muted below. "custom/notification" = { exec = "swaync-client -swb"; return-type = "json"; exec-if = "which swaync-client"; format = "{icon}"; format-icons = { none = "󰂜"; notification = "󰂚"; dnd-none = "󰂛"; dnd-notification = "󰂛"; inhibited-none = "󰂜"; inhibited-notification = "󰂚"; dnd-inhibited-none = "󰂛"; dnd-inhibited-notification = "󰂛"; }; on-click = "swaync-client -t -sw"; on-click-right = "swaync-client -d -sw"; tooltip = true; escape = true; }; tray.spacing = 8; }; generatedStyle = '' /* Palette baked from theme-state.json */ ${colorDefs} * { font-family: "${t.fonts.ui}", "${t.fonts.mono}"; font-size: ${toString t.fonts.size}pt; min-height: 0; } window#waybar { background: alpha(@base, 0.85); color: @text; border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4); border-radius: ${toString t.ui.rounding}px; } #workspaces button { padding: 0 8px; color: @muted; border-radius: ${toString t.ui.rounding}px; } #workspaces button.active { color: @base; background: @accent; } #workspaces button.urgent { color: @base; background: @bad; } #window { color: @subtext; padding: 0 12px; } #clock { color: @text; font-weight: bold; } #tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #language, #battery, #custom-notification { color: @subtext; padding: 0 10px; } /* The power-profile speedometer glyph renders small in its em box — size it up so it reads at a glance like the other indicators. */ #custom-powerprofile { font-size: ${toString (t.fonts.size + 3)}pt; } /* notifications waiting → accent; Do-Not-Disturb → muted bell-off */ #custom-notification.notification { color: @accent; } #custom-notification.dnd-none, #custom-notification.dnd-notification { color: @muted; } #pulseaudio.muted { color: @muted; } #battery.warning { color: @warn; } #battery.critical { color: @bad; } #battery.charging { color: @good; } ''; in { programs.waybar = lib.mkIf config.nomarchy.waybar.enable { enable = true; # Launched from Hyprland's exec-once (hyprland.nix), NOT a systemd user # service. Bound to graphical-session.target the unit raced Hyprland's IPC # on a warm relogin — it started before the socket was up, exited, landed # in `failed`, and was never retried, so the bar vanished. exec-once only # fires once Hyprland is up, dodging the race; theme switches reload the # running bar via SIGUSR2 (nomarchy-theme-sync). No uwsm here to manage the # session target, so we don't depend on its lifecycle. systemd.enable = false; # mkDefault so downstream can replace the whole bar config/style with # a plain home.nix assignment. For per-theme identity, prefer the # whole-swap assets (themes//waybar.{jsonc,css}); for one-off # tweaks of the generated bar, lib.mkForce a sub-key. See # docs/OVERRIDES.md. settings.mainBar = lib.mkDefault ( if hasConfigOverride then builtins.fromJSON (builtins.readFile configOverride) else generatedSettings ); style = lib.mkDefault ( if hasStyleOverride then builtins.readFile styleOverride else generatedStyle ); }; # The power-profile helpers on PATH, so both the generated bar and the # whole-swap themes' static waybar.jsonc can exec them by name. home.packages = lib.optionals config.nomarchy.waybar.enable [ powerProfileStatus powerProfileCycle ]; }