feat(desktop): add the Waybar status bar (task 0023) #15

Merged
alexion merged 4 commits from task-0023-waybar-status-bar into main 2026-07-22 15:28:13 -04:00
4 changed files with 181 additions and 4 deletions
Showing only changes of commit b21b4ff77e - Show all commits

View File

@@ -13,7 +13,28 @@ The do-not-disturb toggle and media controls live in the bar rather than in a se
## Acceptance criteria ## Acceptance criteria
- [ ] A Waybar module exists in the desktop group and is enabled by the aggregator. - [x] A Waybar module exists in the desktop group and is enabled by the aggregator.
- [ ] The bar shows workspaces with per-application icons, a clock, battery, network, audio, MPRIS media controls, and a do-not-disturb toggle. - [x] The bar shows workspaces with per-application icons, a clock, battery, network, audio, MPRIS media controls, and a do-not-disturb toggle.
- [ ] No overview/exposé plugin is used. - [x] No overview/exposé plugin is used.
- [ ] neogaia builds green under `nix flake check`. - [x] neogaia builds green under `nix flake check`.
## Implementation Notes
- **Audio server added beyond the plan.**
The task asked only for the bar's audio *widget*, but nothing in the epic provisions an audio server, and a `wireplumber` widget over a machine with no running sink is inert.
A small `modules/desktop/audio.nix` therefore enables PipeWire (with the ALSA and PulseAudio compatibility shims and rtkit) under its own `modules.desktop.audio.enable`, wired into the aggregator at default priority like every other piece.
It is a distinct concern that could equally live in its own task, so it is flagged here and in the PR for the operator to split out or keep.
- **Do-not-disturb depends on mako, which lands later.**
The `custom/dnd` widget shells out to `makoctl`, whose daemon arrives with the notifications task (0025).
The status script pins mako's store path and degrades to "notifications on" whenever no daemon answers, so the widget is inert rather than broken before 0025 and reflects real state the moment mako runs.
- **Glyphs decoded, not pasted.**
Nerd-font module icons are Private-Use-Area codepoints that do not survive an editor paste, so a `g = code: builtins.fromJSON ''"\u${code}"''` helper decodes each one to real bytes.
`nerd-fonts.symbols-only` is installed system-wide as the pango fallback for those codepoints, since Stylix's monospace font does not carry them.
- **Bar launch.**
The bar runs as a home-manager systemd user service bound to `graphical-session.target`, which uwsm activates, so it comes up with the session without a compositor `exec-once`.
- **Runtime checks deferred to the machine.**
`nix flake check` proves the config evaluates and the host builds, but the rendered bar, the MPRIS widget, and the audio widget can only be exercised in a live Wayland session on neogaia.

19
modules/desktop/audio.nix Normal file
View File

@@ -0,0 +1,19 @@
{ config, lib, ... }:
# PipeWire as the desktop audio server.
let
cfg = config.modules.desktop.audio;
in
{
options.modules.desktop.audio.enable = lib.mkEnableOption "the PipeWire audio server";
config = lib.mkIf cfg.enable {
# Realtime scheduling for the audio threads, so playback survives load.
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
pulse.enable = true;
};
};
}

View File

@@ -13,5 +13,7 @@ in
modules.desktop.login.enable = lib.mkDefault true; modules.desktop.login.enable = lib.mkDefault true;
modules.desktop.terminal.enable = lib.mkDefault true; modules.desktop.terminal.enable = lib.mkDefault true;
modules.desktop.theming.enable = lib.mkDefault true; modules.desktop.theming.enable = lib.mkDefault true;
modules.desktop.waybar.enable = lib.mkDefault true;
modules.desktop.audio.enable = lib.mkDefault true;
}; };
} }

135
modules/desktop/waybar.nix Normal file
View File

@@ -0,0 +1,135 @@
{
config,
lib,
pkgs,
...
}:
# Waybar: a top status bar reading system state at a glance.
let
cfg = config.modules.desktop.waybar;
user = config.user.name;
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
# through JSON so the glyph survives as bytes rather than an editor paste.
g = code: builtins.fromJSON ''"\u${code}"'';
# Renders and toggles mako's do-not-disturb mode.
# With no mako daemon answering, the status reads "notifications on", keeping
# the widget inert rather than broken.
dndStatus = pkgs.writeShellScript "waybar-dnd-status" ''
if ${pkgs.mako}/bin/makoctl mode 2>/dev/null | ${pkgs.gnugrep}/bin/grep -qx dnd; then
printf '{"text":"${g "f1f6"}","tooltip":"Do not disturb: on","class":"dnd"}\n'
else
printf '{"text":"${g "f0f3"}","tooltip":"Notifications on","class":"active"}\n'
fi
'';
dndToggle = pkgs.writeShellScript "waybar-dnd-toggle" ''
${pkgs.mako}/bin/makoctl mode -t dnd >/dev/null 2>&1
${pkgs.procps}/bin/pkill -RTMIN+8 waybar
'';
in
{
options.modules.desktop.waybar.enable = lib.mkEnableOption "the Waybar status bar";
config = lib.mkIf cfg.enable {
# A symbols font backs the module glyphs, so pango falls back to it for the
# codepoints Stylix's monospace font does not carry.
fonts.packages = [ pkgs.nerd-fonts.symbols-only ];
home-manager.users.${user}.programs.waybar = {
enable = true;
# A user service bound to graphical-session.target, which uwsm activates,
# so the bar comes up with the session and no compositor exec-once.
systemd.enable = true;
settings.mainBar = {
layer = "top";
position = "top";
height = 30;
modules-left = [ "hyprland/workspaces" ];
modules-center = [ "clock" ];
modules-right = [
"mpris"
"wireplumber"
"network"
"battery"
"custom/dnd"
];
# Per-application icons for the windows on each workspace, mapped from
# window class with a neutral glyph for anything unlisted.
"hyprland/workspaces" = {
on-click = "activate";
format = "{id} {windows}";
format-window-separator = " ";
window-rewrite-default = g "f2d0";
window-rewrite = {
"class<Alacritty>" = g "f120";
"class<.*[Ff]irefox.*>" = g "f269";
"class<chromium.*>" = g "f268";
"class<[Cc]ode>" = g "f121";
};
};
clock = {
format = "{:%a %d %b %H:%M}";
tooltip-format = "<tt>{calendar}</tt>";
};
mpris = {
format = "${g "f001"} {title}";
format-paused = "${g "f04c"} {title}";
max-length = 40;
on-click = "play-pause";
on-scroll-up = "next";
on-scroll-down = "previous";
};
wireplumber = {
format = "{icon} {volume}%";
format-muted = "${g "f026"} muted";
format-icons = [
(g "f026")
(g "f027")
(g "f028")
];
on-click = "${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
};
network = {
format-wifi = "${g "f1eb"} {essid}";
format-ethernet = "${g "f796"} {ifname}";
format-disconnected = "${g "f127"} off";
tooltip-format = "{ipaddr} via {gwaddr}";
};
battery = {
format = "{icon} {capacity}%";
format-charging = "${g "f0e7"} {capacity}%";
format-icons = [
(g "f244")
(g "f243")
(g "f242")
(g "f241")
(g "f240")
];
states = {
warning = 20;
critical = 10;
};
};
"custom/dnd" = {
return-type = "json";
exec = "${dndStatus}";
on-click = "${dndToggle}";
interval = "once";
signal = 8;
};
};
};
};
}