251 lines
7.5 KiB
Nix
251 lines
7.5 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
# Waybar: a top status bar reading system state at a glance.
|
|
let
|
|
cfg = config.modules.desktop.session.waybar; # NOLINT(let_order)
|
|
vpnClient = config.modules.network.vpn-client;
|
|
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}"'';
|
|
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
|
|
|
# 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
|
|
'';
|
|
|
|
# Lit while a wf-recorder capture is running.
|
|
# Empty text collapses the widget when idle, so it shows nothing until then.
|
|
recStatus = pkgs.writeShellScript "waybar-recording-status" ''
|
|
if ${pkgs.procps}/bin/pgrep -x wf-recorder >/dev/null; then
|
|
printf '{"text":"${g "f03d"}","tooltip":"Recording","class":"recording"}\n'
|
|
else
|
|
printf '{"text":"","class":"idle"}\n'
|
|
fi
|
|
'';
|
|
|
|
idleLockStatus = pkgs.writeShellScript "waybar-idle-lock-status" ''
|
|
if ${pkgs.systemd}/bin/systemctl --user is-active --quiet lockscreen-idle-inhibit.service; then
|
|
printf '{"text":"${sleepOffIcon}","tooltip":"Idle locking inhibited","class":"inhibited"}\n'
|
|
else
|
|
printf '{"text":"","class":"idle"}\n'
|
|
fi
|
|
'';
|
|
|
|
vpnStatus = pkgs.writeShellScript "waybar-vpn-status" ''
|
|
interface=${lib.escapeShellArg vpnClient.interface}
|
|
|
|
if ! ${pkgs.iproute2}/bin/ip link show dev "$interface" >/dev/null 2>&1; then
|
|
printf '{"text":"","class":"idle"}\n'
|
|
exit 0
|
|
fi
|
|
|
|
ipaddr=$(${pkgs.iproute2}/bin/ip -4 -o addr show dev "$interface" | ${pkgs.gawk}/bin/awk '{ print $4; exit }')
|
|
|
|
${pkgs.jq}/bin/jq -cn \
|
|
--arg text "" \
|
|
--arg tooltip "VPN $interface: ''${ipaddr:-no IPv4}" \
|
|
--arg class "active" \
|
|
'{ text: $text, tooltip: $tooltip, class: $class }'
|
|
'';
|
|
|
|
networkStatus = pkgs.writeShellScript "waybar-network-status" ''
|
|
active=$(
|
|
${pkgs.networkmanager}/bin/nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status \
|
|
| ${pkgs.gawk}/bin/awk -F: '$3 == "connected" && $2 != "loopback" { print; exit }'
|
|
)
|
|
|
|
if [ -z "$active" ]; then
|
|
${pkgs.jq}/bin/jq -cn \
|
|
--arg text "${g "f127"} off" \
|
|
--arg tooltip "Network disconnected" \
|
|
--arg class "disconnected" \
|
|
'{ text: $text, tooltip: $tooltip, class: $class }'
|
|
exit 0
|
|
fi
|
|
|
|
IFS=: read -r device type _state connection <<< "$active"
|
|
|
|
case "$type" in
|
|
wifi) icon="${g "f1eb"}" ;;
|
|
ethernet) icon="LAN" ;;
|
|
*) icon="NET" ;;
|
|
esac
|
|
|
|
ipaddr=$(${pkgs.iproute2}/bin/ip -4 -o addr show dev "$device" | ${pkgs.gawk}/bin/awk '{ print $4; exit }')
|
|
|
|
${pkgs.jq}/bin/jq -cn \
|
|
--arg text "$icon $connection" \
|
|
--arg tooltip "$device: ''${ipaddr:-no IPv4}" \
|
|
--arg class "$type" \
|
|
'{ text: $text, tooltip: $tooltip, class: $class }'
|
|
'';
|
|
in
|
|
{
|
|
options.modules.desktop.session.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;
|
|
|
|
style = ''
|
|
.modules-right {
|
|
margin-right: 8px;
|
|
}
|
|
|
|
#custom-dnd {
|
|
margin-right: 8px;
|
|
}
|
|
'';
|
|
|
|
settings.mainBar = {
|
|
layer = "top";
|
|
position = "top";
|
|
height = 30;
|
|
spacing = 8;
|
|
|
|
modules-left = [ "hyprland/workspaces" ];
|
|
modules-center = [ "clock" ];
|
|
modules-right = [
|
|
"custom/recording"
|
|
"custom/idle-lock"
|
|
"mpris"
|
|
"wireplumber"
|
|
]
|
|
++ lib.optional vpnClient.enable "custom/vpn"
|
|
++ [
|
|
"custom/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<com.mitchellh.ghostty>" = g "f120";
|
|
"class<.*[Ff]irefox.*>" = g "f269";
|
|
"class<chromium.*>" = g "f268";
|
|
"class<[Cc]ode>" = g "f121";
|
|
"class<obsidian>" = g "f02d";
|
|
"class<[Ss]ignal>" = g "f4ad";
|
|
"class<[Ss]team>" = g "f1b6";
|
|
"class<[Vv]esktop>" = g "f1ff";
|
|
};
|
|
};
|
|
|
|
clock = {
|
|
format = "{:%a %d %b %H:%M}";
|
|
tooltip-format = "<tt>{calendar}</tt>";
|
|
calendar = {
|
|
mode = "month";
|
|
# Underline the current day so it stands out in the grid.
|
|
format.today = "<b><u>{}</u></b>";
|
|
};
|
|
};
|
|
|
|
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";
|
|
};
|
|
|
|
"custom/vpn" = {
|
|
return-type = "json";
|
|
exec = "${vpnStatus}";
|
|
interval = 5;
|
|
};
|
|
|
|
"custom/network" = {
|
|
return-type = "json";
|
|
exec = "${networkStatus}";
|
|
interval = 5;
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
# Recording state has no event to hook, so the widget samples the
|
|
# wf-recorder process once a second.
|
|
"custom/recording" = {
|
|
return-type = "json";
|
|
exec = "${recStatus}";
|
|
interval = 1;
|
|
};
|
|
|
|
"custom/idle-lock" = {
|
|
return-type = "json";
|
|
exec = "${idleLockStatus}";
|
|
interval = "once";
|
|
signal = 9;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|