The generated bar (every non-whole-swap theme; whole-swaps untouched): - The active-workspace highlighter filled the bar's full inner height and visually overhung the rounded border. The pill now gets a 3px vertical inset margin (the boreal whole-swap's proven pattern) and the bar grows 34 -> 36 so the inset doesn't cramp the number. - The end modules sat hard against the bar's rounded corners (the power button ~8px from the right curve vs ~28px inter-icon gaps). The Nomarchy mark and the power button get wider outer padding (14/16px); the item-28c group rhythm is preserved. Verification: V2. nix flake check --no-build green. §3 protocol: before/after theme-shot under kanagawa (generated palette) viewed at 2x crops — before: pill overhangs top+bottom, power glyph kisses the corner; after: pill fully inside the bar, clear end gap (before /nix/store/iwrvzzdd...theme-shot-kanagawa, after /nix/store/za...see scratchpad shot-kanagawa-after.path). No-regression capture under the summer-night whole-swap viewed — its chip identity unchanged (whole-swaps replace both style and layout, out of scope per the item). BACKLOG #152 closed: deleted per the finished-items rule in the next bookkeeping touch of this push. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
581 lines
25 KiB
Nix
581 lines
25 KiB
Nix
# Waybar — two-tier theming:
|
||
#
|
||
# 1. Default: structure, fonts, geometry AND palette baked from
|
||
# state.json (colors as GTK named colors, @define-color).
|
||
#
|
||
# 2. Whole-swap: themes with their own visual identity ship
|
||
# <themesDir>/<slug>/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;
|
||
termSheet = import ./term-sheet.nix { inherit pkgs; };
|
||
|
||
# Whether the keyboard-layout indicator is worth bar space, and the filter
|
||
# that holds a whole-swap to the same answer. Contract + rationale (and
|
||
# checks.waybar-language) live in ./waybar-language.nix.
|
||
langLib = import ./waybar-language.nix { inherit lib; };
|
||
showLanguage = langLib.show {
|
||
layout = config.nomarchy.keyboard.layout;
|
||
devices = config.nomarchy.keyboard.devices;
|
||
remembered = config.nomarchy.settings.keyboard.devices or { };
|
||
};
|
||
gateLanguage = langLib.gate showLanguage;
|
||
|
||
# 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.
|
||
# Waybar supervisor — exec-once has no restart, so a crashed bar used to
|
||
# leave the session bar-less until relogin (seen on hardware: a theme
|
||
# switch crashed waybar mid-reload). Respawns on ANY exit — a plain
|
||
# `pkill -x waybar` is now a clean restart, which nomarchy-state-sync
|
||
# uses instead of the crash-prone in-place SIGUSR2 reload when it sees
|
||
# this supervisor running. Crash-loop guard: 5 exits within 10s of
|
||
# their start → give up with a critical notification instead of
|
||
# spinning. Stop the bar for real: pkill -f nomarchy-waybar (TERM is
|
||
# trapped to take the child down too).
|
||
waybarSupervisor = pkgs.writeShellScriptBin "nomarchy-waybar" ''
|
||
child=
|
||
trap '[ -n "$child" ] && kill "$child" 2>/dev/null; exit 0' TERM INT
|
||
fails=0
|
||
while :; do
|
||
start=$(date +%s)
|
||
waybar & child=$!
|
||
wait "$child"; code=$?
|
||
if [ $(( $(date +%s) - start )) -lt 10 ]; then
|
||
fails=$((fails + 1))
|
||
if [ "$fails" -ge 5 ]; then
|
||
notify-send -u critical "Waybar" \
|
||
"Crashing on start (exit $code) — check ~/.config/waybar. Giving up." 2>/dev/null
|
||
exit 1
|
||
fi
|
||
else
|
||
fails=0
|
||
fi
|
||
sleep 1
|
||
done
|
||
'';
|
||
|
||
powerProfileStatus = pkgs.writeShellScriptBin "nomarchy-powerprofile-status" ''
|
||
# Name-agnostic system battery (BAT0, CMB0, …) — BACKLOG #60.
|
||
has_bat=
|
||
for d in /sys/class/power_supply/*/; do
|
||
[ "$(cat "$d/type" 2>/dev/null)" = Battery ] || continue
|
||
[ "$(cat "$d/scope" 2>/dev/null || echo System)" = Device ] && continue
|
||
has_bat=1; break
|
||
done
|
||
[ -n "$has_bat" ] || exit 0
|
||
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"
|
||
'';
|
||
|
||
# Opens calcurse (lightweight TUI calendar) in a floating, centered kitty
|
||
# window — Waybar clock on-click. Distinct --class → hyprland.nix float
|
||
# rules. Kitty is always installed (kitty.nix). The sheet sizes itself
|
||
# (#139: a percentage `size` rule is silently ignored) — see term-sheet.nix.
|
||
calendarLauncher = pkgs.writeShellScriptBin "nomarchy-calendar" ''
|
||
if ! command -v calcurse >/dev/null 2>&1; then
|
||
notify-send "Calendar" "calcurse isn't installed (removed from home.packages?)." 2>/dev/null
|
||
exit 0
|
||
fi
|
||
exec ${termSheet}/bin/nomarchy-term-sheet com.nomarchy.calendar 60 65 calcurse
|
||
'';
|
||
|
||
# VPN indicator — shows a shield when a NetworkManager VPN/WireGuard
|
||
# connection is active OR Tailscale is up; prints nothing otherwise so the
|
||
# module self-hides (like nightlight/updates). The tooltip names each
|
||
# active VPN (NM connection names, "Tailscale") — without it a forgotten
|
||
# tailscaled reads as "the icon is stuck". Click opens the VPN submenu.
|
||
vpnStatus = pkgs.writeShellScriptBin "nomarchy-vpn-status" ''
|
||
names=$(nmcli -t -f NAME,TYPE connection show --active 2>/dev/null \
|
||
| awk -F: '$2=="vpn"||$2=="wireguard"{print $1}')
|
||
ts=""
|
||
if command -v tailscale >/dev/null 2>&1; then
|
||
[ "$(tailscale status --json 2>/dev/null | jq -r '.BackendState // empty')" = Running ] && ts=Tailscale
|
||
fi
|
||
[ -n "$names$ts" ] || exit 0
|
||
list=$(printf '%s\n' "$names" "$ts" | grep -v '^$' | paste -sd ', ' -)
|
||
tip=$(printf 'VPN: %s\n(click to manage)' "$list" | jq -Rs 'rtrimstr("\n")')
|
||
printf '{"text":"","tooltip":%s,"class":"on"}\n' "$tip"
|
||
'';
|
||
|
||
# Health warning — self-gates: prints nothing while nomarchy-doctor
|
||
# exits 0, so the module only appears when the sheet has a ✖ (the
|
||
# same self-hide discipline as vpn/nightlight/updates). The tooltip
|
||
# carries the first failing lines; click opens the full sheet.
|
||
# Absolute path to the doctor binary: waybar's custom-module env can
|
||
# miss system PATH, and `command -v … || exit 0` then self-hides forever.
|
||
doctorStatus = pkgs.writeShellScriptBin "nomarchy-doctor-status" ''
|
||
out=$(${pkgs.nomarchy-doctor}/bin/nomarchy-doctor 2>/dev/null) && exit 0
|
||
# Strip ANSI so the tooltip is plain text (and waybar never chokes on
|
||
# control chars); keep only the ✖ lines.
|
||
tip=$(printf '%s\n' "$out" \
|
||
| ${pkgs.gnused}/bin/sed 's/\x1b\[[0-9;]*m//g' \
|
||
| grep '✖' | head -5 \
|
||
| ${pkgs.jq}/bin/jq -Rs 'rtrimstr("\n") + "\n(click for the full sheet)"')
|
||
# (md-alert-circle): alert shape that survives incomplete Nerd Font
|
||
# cuts better than ; class:bad still paints it @bad.
|
||
printf '{"text":"","tooltip":%s,"class":"bad"}\n' "$tip"
|
||
'';
|
||
|
||
# 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 = {
|
||
# `bottom`, not `top`: on Hyprland the `top` layer renders above every
|
||
# window — so a real fullscreen surface (a browser video gone
|
||
# fullscreen) sits *under* the bar. On `bottom` the fullscreen window
|
||
# covers the bar, while the exclusive zone still reserves its space in
|
||
# normal tiling. Keep in sync with the whole-swap jsoncs (parity rule).
|
||
layer = "bottom";
|
||
position = "top";
|
||
# 36, not 34: the active-workspace pill is inset 3px top+bottom (CSS
|
||
# below) so it can never overhang the bar's border — the extra height
|
||
# keeps the pill's text from cramping inside the inset (#152).
|
||
height = 36;
|
||
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;
|
||
|
||
# Logo + powermenu: whole-swaps already ship these (parity was reverse
|
||
# — BACKLOG #63). Click targets are existing nomarchy-menu entry points.
|
||
modules-left = [ "custom/nomarchy" "hyprland/workspaces" "hyprland/window" ];
|
||
modules-center = [ "clock" ];
|
||
modules-right = [ "custom/recording" "idle_inhibitor" "tray" "custom/vpn" "custom/airplane" "pulseaudio" "custom/powerprofile" "custom/nightlight" ]
|
||
++ lib.optional showLanguage "hyprland/language"
|
||
++ [ "battery" "custom/doctor" "custom/updates" "custom/notification" "custom/powermenu" ];
|
||
|
||
"custom/nomarchy" = {
|
||
interval = "once";
|
||
# U+F000 — Nomarchy monogram (literal UTF-8; Nix has no \u escapes).
|
||
# CSS pins font-family: Nomarchy so Nerd Fonts' glass glyph does not win.
|
||
format = "";
|
||
on-click = "nomarchy-menu";
|
||
tooltip-format = "Nomarchy menu";
|
||
};
|
||
|
||
"custom/powermenu" = {
|
||
format = ""; # U+F011 power symbol
|
||
on-click = "nomarchy-menu power";
|
||
tooltip = false;
|
||
};
|
||
|
||
"hyprland/workspaces" = {
|
||
format = "{icon}";
|
||
on-click = "activate";
|
||
};
|
||
|
||
"hyprland/window" = {
|
||
max-length = 48;
|
||
separate-outputs = true;
|
||
};
|
||
|
||
clock = {
|
||
# Single module: time + short date (whole-swaps used to split these
|
||
# into clock + clock#date — one click surface for the calendar).
|
||
format = "{:%H:%M · %a %d %b}";
|
||
# Left-click → the calendar (nomarchy-calendar → calcurse in a floating
|
||
# kitty). Tooltip is plain date/zone only — embedding {calendar}
|
||
# produced an empty popup on hardware (2026-07-10); month view is
|
||
# one click away.
|
||
on-click = "nomarchy-calendar";
|
||
tooltip = true;
|
||
# One chrono block only: waybar/fmt empties the tooltip when two
|
||
# bare `{:%…}` specs are concatenated (bar format works because it
|
||
# is a single block). Newline + zone live under auto-timezone
|
||
# (tz-watch SIGUSR2 reload).
|
||
tooltip-format = "{:%A, %d %B %Y\n%Z (UTC%z)}";
|
||
};
|
||
|
||
# Active keyboard layout (per focused device) — only placed in
|
||
# modules-right when showLanguage (see above).
|
||
"hyprland/language" = {
|
||
# Two spaces after the glyph: the keyboard icon's right bearing
|
||
# otherwise kisses the layout code (esp. mono Nerd faces).
|
||
format = "<span size='${toString (t.fonts.size + 2)}pt'> </span>{short}";
|
||
tooltip = false;
|
||
};
|
||
|
||
pulseaudio = {
|
||
# Two trailing spaces on every icon level: high-volume glyphs are the
|
||
# widest and still kissed the % with a single pad (hardware report).
|
||
format = "<span size='${toString (t.fonts.size + 2)}pt'>{icon}</span>{volume}%";
|
||
format-muted = "";
|
||
format-icons.default = [ " " " " " " ];
|
||
on-click = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
||
# Right-click → the full mixer (per-app volumes) in a floating window
|
||
# (item 35). pwvucontrol ships in the template suite; the Hyprland
|
||
# windowrule floats it. Keep in sync with the whole-swap jsoncs.
|
||
on-click-right = "pwvucontrol";
|
||
};
|
||
|
||
# Idle inhibitor (caffeine): click → to hold the screen awake —
|
||
# blocks hypridle's lock / display-off / suspend during video or a
|
||
# presentation. Waybar holds a Wayland idle-inhibit while activated;
|
||
# the state resets with the bar (deliberate — caffeine shouldn't
|
||
# survive a relogin). Summer-night's whole-swap bar had this first
|
||
# (reverse parity gap, 2026-07-04).
|
||
idle_inhibitor = {
|
||
format = "{icon}";
|
||
format-icons = { activated = ""; deactivated = ""; };
|
||
tooltip-format-activated = "Screen held awake — click to release";
|
||
tooltip-format-deactivated = "Keep the screen awake (caffeine)";
|
||
};
|
||
|
||
# No network module: nm-applet lives in the tray (the GUI path), so the
|
||
# bar's wifi/ethernet indicator would just duplicate it.
|
||
|
||
battery = {
|
||
states = { warning = 25; critical = 10; };
|
||
# Same double-pad as pulseaudio (single space still tight on some glyphs).
|
||
format = "<span size='${toString (t.fonts.size + 2)}pt'>{icon}</span>{capacity}%";
|
||
format-charging = "<span size='${toString (t.fonts.size + 2)}pt'> </span>{capacity}%";
|
||
format-icons = [ " " " " " " " " " " ];
|
||
# The kernel's "Not charging" maps to waybar's Plugged state — on a
|
||
# machine with a charge cap that's the hold band doing its job
|
||
# (power.nix sets start = cap − 10), which reads as "broken charger"
|
||
# without an explanation. The cap value isn't baked here: the menu
|
||
# can change it live (sysfs + state, no rebuild), so a number would
|
||
# go stale.
|
||
tooltip-format-plugged = "Plugged in, not charging — held at {capacity}% by the battery charge cap\nCharging resumes ~10% below the cap · click to adjust";
|
||
# Click either the battery or the power-profile icon → the combined
|
||
# power menu (profile + charge cap). The granular System rows stay.
|
||
on-click = "nomarchy-menu powermgmt";
|
||
};
|
||
|
||
"custom/powerprofile" = {
|
||
exec = "nomarchy-powerprofile-status";
|
||
return-type = "json";
|
||
interval = 5;
|
||
# Opens the same combined power menu as the battery icon (item 36b).
|
||
# nomarchy-powerprofile-cycle stays a standalone bin for downstream
|
||
# rebinding — it's just no longer the default click.
|
||
on-click = "nomarchy-menu powermgmt";
|
||
};
|
||
|
||
# VPN shield — self-hides unless a NM VPN/WireGuard tunnel or Tailscale is
|
||
# up. Click opens the VPN submenu (nomarchy-vpn). 5s poll like powerprofile.
|
||
"custom/vpn" = {
|
||
exec = "nomarchy-vpn-status";
|
||
return-type = "json";
|
||
interval = 5;
|
||
on-click = "nomarchy-vpn";
|
||
};
|
||
|
||
# Health check is real work (systemctl/disk/git sweeps), so a long
|
||
# interval — this is a tripwire, not a monitor.
|
||
"custom/doctor" = {
|
||
exec = "nomarchy-doctor-status";
|
||
return-type = "json";
|
||
format = "{}";
|
||
interval = 300;
|
||
# signal 10: poke after a fix (or from theme-shot) so the tripwire
|
||
# doesn't wait a full interval after a first-poll miss.
|
||
signal = 10;
|
||
on-click = "nomarchy-menu doctor";
|
||
};
|
||
|
||
# Screen recording indicator. Self-gates: visible only while
|
||
# nomarchy-record runs (status prints nothing otherwise), and the
|
||
# click IS the stop surface — the menu only starts recordings.
|
||
# signal 8: the recorder pokes the bar on start/stop so the ⏺
|
||
# appears/vanishes instantly (the interval is just a safety net).
|
||
"custom/recording" = {
|
||
exec = "nomarchy-record status";
|
||
return-type = "json";
|
||
interval = 10;
|
||
signal = 8;
|
||
on-click = "nomarchy-record stop";
|
||
};
|
||
|
||
# Night-light (hyprsunset) indicator. Self-gates: the moon shows only while
|
||
# the schedule runs; otherwise the status helper prints nothing => hidden
|
||
# (enable / re-enable from the System menu). Click toggles instantly — writes
|
||
# the in-flake on/off (settings.nightlight.on, no rebuild) and flips the unit
|
||
# with systemctl, so the choice lands in the flake and survives reboot.
|
||
"custom/nightlight" = {
|
||
exec = "nomarchy-nightlight status";
|
||
return-type = "json";
|
||
interval = 3;
|
||
on-click = "nomarchy-nightlight toggle";
|
||
};
|
||
|
||
# Airplane mode (#104). Self-gates: plane glyph only while engaged
|
||
# (status prints nothing otherwise). Click toggles Wi-Fi+BT and
|
||
# restores prior radio state on disengage. signal 11 = instant refresh.
|
||
"custom/airplane" = {
|
||
exec = "nomarchy-airplane status";
|
||
return-type = "json";
|
||
interval = 5;
|
||
signal = 11;
|
||
on-click = "nomarchy-airplane toggle";
|
||
};
|
||
|
||
# Update awareness. Self-gates: hidden unless nomarchy.updates is enabled
|
||
# AND the periodic check found something (the helper prints nothing then).
|
||
# signal 9 lets the checker refresh it instantly; click opens the upgrade
|
||
# flow in a terminal.
|
||
"custom/updates" = {
|
||
exec = "nomarchy-updates status";
|
||
return-type = "json";
|
||
interval = 1800;
|
||
signal = 9;
|
||
on-click = "nomarchy-updates upgrade-window";
|
||
};
|
||
|
||
# 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.
|
||
# Every *suppressed* state — DND or app-inhibited — maps to the bell-off
|
||
# glyph and the muted color below, so "notifications are off right now"
|
||
# reads by SHAPE, not color alone (color-blind sweep, item 28): inhibited
|
||
# used to reuse the normal / bells, distinguishable only by color.
|
||
"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 state.json */
|
||
${colorDefs}
|
||
|
||
/* NB: this `*` reset reaches the SNI tray menus Waybar hosts too
|
||
(its stylesheet applies process-wide, at a priority user gtk.css
|
||
cannot out-rank). The menu block right below undoes the damage —
|
||
keep the two in sync. Do NOT scope `*` to window#waybar instead:
|
||
the id's specificity would beat the class rules below and wreck
|
||
the bar. Whole-swap theme waybar.css files carry the same pair. */
|
||
* {
|
||
font-family: "${t.fonts.ui}", "${t.fonts.mono}";
|
||
font-size: ${toString t.fonts.size}pt;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* Tray menus: undo the `*` reset — same stylesheet, higher
|
||
specificity. Arrows/checks/separators are CSS-sized nodes;
|
||
min-height 0 makes them invisible. */
|
||
menu menuitem arrow { min-width: 16px; min-height: 16px; }
|
||
menu check, menu radio { min-width: 14px; min-height: 14px; }
|
||
menu separator {
|
||
min-height: 1px;
|
||
margin: 5px 0;
|
||
background: alpha(@text, 0.15);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/* Dim states use the palette's @muted role: since item 28b it is
|
||
floor-guaranteed legible on @base in every theme (muted/base >=
|
||
2.0, gated by tools/check-theme-contrast.py) — the palettes that
|
||
once made it vanish (gruvbox muted≈base, item 27) were retuned.
|
||
Secondary-but-not-dim stays alpha(@text, 0.85). */
|
||
/* 3px vertical margin insets the active pill INSIDE the bar — without
|
||
it the filled button spans the bar's full inner height and visually
|
||
overhangs the rounded border (#152). */
|
||
#workspaces button {
|
||
padding: 0 8px;
|
||
margin: 3px 0;
|
||
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: alpha(@text, 0.85);
|
||
padding: 0 12px;
|
||
}
|
||
|
||
#clock {
|
||
color: @text;
|
||
font-weight: bold;
|
||
}
|
||
|
||
#custom-nomarchy {
|
||
color: @accent;
|
||
font-family: Nomarchy;
|
||
font-size: ${toString (t.fonts.size + 4)}pt;
|
||
/* Wider outer padding: the bar's rounded corner curves into the
|
||
end module's box, so the edge glyphs need more room than the
|
||
uniform 10px or they look cramped (#152; power button mirrors). */
|
||
padding: 0 10px 0 14px;
|
||
}
|
||
#custom-nomarchy:hover { color: @accentAlt; }
|
||
|
||
#tray, #pulseaudio, #custom-powerprofile, #custom-nightlight, #custom-airplane, #custom-updates, #custom-vpn, #custom-recording, #idle_inhibitor, #language, #battery, #custom-doctor, #custom-notification, #custom-powermenu {
|
||
color: alpha(@text, 0.85);
|
||
padding: 0 10px;
|
||
}
|
||
#custom-powermenu { padding: 0 16px 0 10px; }
|
||
#custom-powermenu:hover { color: @bad; }
|
||
|
||
/* Group rhythm (item 28c): a wider breath before each functional
|
||
group of the right cluster — media/stats · toggles · status —
|
||
on top of the uniform module spacing. A group head can self-hide
|
||
(e.g. no battery on desktops); grouping then degrades to the
|
||
uniform spacing, never breaks. */
|
||
#pulseaudio, #custom-powerprofile, #battery { margin-left: 14px; }
|
||
|
||
/* Caffeine engaged → warm tone (the screen is being held awake). */
|
||
#idle_inhibitor.activated { color: @warn; }
|
||
|
||
/* Recording in progress → the alert red; the ⏺ is also the stop button. */
|
||
#custom-recording.recording { color: @bad; }
|
||
|
||
/* VPN active → accent green, reading as "connected / protected". */
|
||
#custom-vpn.on { color: @good; }
|
||
|
||
/* Night-light active → warm tone, matching the filter it represents. */
|
||
#custom-nightlight.on { color: @warn; }
|
||
#custom-airplane.on { color: @warn; }
|
||
|
||
/* Updates pending → accent, to draw the eye. */
|
||
#custom-updates.available { color: @accent; }
|
||
|
||
/* Doctor tripwire — only rendered when something is ✖, so it is
|
||
always the alert color. */
|
||
#custom-doctor { color: @bad; }
|
||
|
||
/* Icon-only status modules carry no text, so they don't get the
|
||
(size+2)pt Pango icon span the icon+text modules use — bump their
|
||
font-size to match, or these glyphs read smaller than the volume /
|
||
battery / language icons beside them. */
|
||
#custom-recording, #custom-updates, #custom-vpn, #custom-airplane, #custom-nightlight, #custom-doctor, #custom-notification, #custom-powermenu { font-size: ${toString (t.fonts.size + 2)}pt; }
|
||
|
||
/* The speedometer + caffeine glyphs render small in their em box —
|
||
size them up a touch more so they read at a glance. */
|
||
#custom-powerprofile, #idle_inhibitor { font-size: ${toString (t.fonts.size + 3)}pt; }
|
||
|
||
/* notifications waiting → accent; suppressed (DND or app-inhibited) →
|
||
muted bell-off, so it reads by shape+color, never color alone. */
|
||
#custom-notification.notification { color: @accent; }
|
||
#custom-notification.dnd-none,
|
||
#custom-notification.dnd-notification,
|
||
#custom-notification.inhibited-none,
|
||
#custom-notification.inhibited-notification { color: @muted; }
|
||
|
||
#pulseaudio.muted { color: @muted; }
|
||
#battery.warning { color: @warn; }
|
||
#battery.critical { color: @bad; }
|
||
#battery.charging { color: @good; }
|
||
|
||
/* Clock (and other) hover tooltips — explicit colors so an empty
|
||
looking box is never just “text same as background”. */
|
||
tooltip {
|
||
background: @surface;
|
||
border: ${toString t.ui.borderSize}px solid alpha(@accent, 0.4);
|
||
border-radius: ${toString t.ui.rounding}px;
|
||
}
|
||
tooltip label { color: @text; }
|
||
'';
|
||
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-state-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/<slug>/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 gateLanguage (builtins.fromJSON (builtins.readFile configOverride))
|
||
else generatedSettings
|
||
);
|
||
|
||
style = lib.mkDefault (
|
||
if hasStyleOverride
|
||
then builtins.readFile styleOverride
|
||
else generatedStyle
|
||
);
|
||
};
|
||
|
||
# Bar helpers on PATH (whole-swap jsoncs exec them by bare name) +
|
||
# feature deps of bar clicks: calcurse (clock), pwvucontrol (volume
|
||
# right-click) — not opt-in template packages.
|
||
home.packages = lib.optionals config.nomarchy.waybar.enable
|
||
[ waybarSupervisor powerProfileStatus powerProfileCycle vpnStatus doctorStatus calendarLauncher
|
||
pkgs.calcurse
|
||
pkgs.pwvucontrol
|
||
];
|
||
}
|