feat(nightlight): menu + Waybar on/off toggle

Add a runtime toggle for the scheduled blue-light filter, the remaining
optional item on the night-light roadmap entry.

nomarchy-nightlight (nightlight.nix, always on PATH) starts/stops the
hyprsunset *service* rather than poking its runtime gamma: `systemctl
--user is-active` is the single source of truth, the screen restores to
true colours when stopped, and it lets you force the filter off for
colour-sensitive work (even at night) and back on without touching config.
It self-gates — `status` prints nothing when the unit is absent — so the
indicator hides itself when night-light isn't enabled.

- Menu: `nomarchy-menu nightlight` toggles it; a self-gated row in the
  System submenu (shown only when the hyprsunset unit exists).
- Waybar: a self-gating `custom/nightlight` indicator (moon = on, sun =
  off), in the generated bar and both summer whole-swap themes for parity.

Verified: home generation builds, generated waybar config carries
custom/nightlight, the menu has the dispatcher case + System row, both
scripts pass bash -n, and status self-gates to empty with no unit. Pending
an on-machine check that stopping hyprsunset cleanly restores the gamma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-06-18 22:23:12 +01:00
parent 09c308b93c
commit 5c43a93285
9 changed files with 102 additions and 13 deletions

View File

@@ -5,14 +5,51 @@
#
# The hyprsunset HM service module is provided by home-manager; this only
# configures it. Override anything with plain services.hyprsunset.* options.
{ config, lib, ... }:
#
# nomarchy-nightlight is the menu/Waybar toggle: it starts/stops the hyprsunset
# *service* (so `systemctl --user is-active` is the single source of truth and
# the screen restores to true colours when stopped), letting you force the
# filter off for colour-sensitive work and back on without touching the config.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.nightlight;
# Toggle/status helper, on PATH so the menu and both the generated and
# whole-swap Waybars can drive it by name. Self-gates: when the unit doesn't
# exist (night-light not enabled) `status` prints nothing, so the Waybar
# module hides itself — same pattern as the power-profile indicator.
nomarchy-nightlight = pkgs.writeShellScriptBin "nomarchy-nightlight" ''
unit=hyprsunset.service
if ! systemctl --user cat "$unit" >/dev/null 2>&1; then
[ "''${1:-}" = status ] && exit 0
notify-send "Night light" "Not enabled set nomarchy.nightlight.enable."
exit 0
fi
case "''${1:-toggle}" in
status)
if systemctl --user is-active --quiet "$unit"; then
printf '{"text":"󰖔","tooltip":"Night light on warm on schedule (click to disable)","class":"on"}\n'
else
printf '{"text":"󰖙","tooltip":"Night light off (click to enable)","class":"off"}\n'
fi ;;
on) systemctl --user start "$unit" ;;
off) systemctl --user stop "$unit" ;;
toggle)
if systemctl --user is-active --quiet "$unit"; then
systemctl --user stop "$unit"
notify-send "Night light off" "Screen back to true colours."
else
systemctl --user start "$unit"
notify-send "Night light on" "Warm filter follows your schedule."
fi ;;
*) echo "usage: nomarchy-nightlight [toggle|status|on|off]" >&2; exit 64 ;;
esac
'';
in
{
config = lib.mkIf cfg.enable {
services.hyprsunset = {
config = {
services.hyprsunset = lib.mkIf cfg.enable {
enable = true;
settings.profile = [
# Daytime: identity = no colour change.
@@ -21,5 +58,9 @@ in
{ time = cfg.sunset; temperature = cfg.temperature; }
];
};
# Always on PATH (it self-gates at runtime), so a static whole-swap Waybar
# can exec it even when night-light is off without the call failing.
home.packages = [ nomarchy-nightlight ];
};
}

View File

@@ -178,6 +178,14 @@ let
&& notify-send "Do Not Disturb off" "Notifications resumed."
exit 0 ;;
nightlight)
# Force the scheduled blue-light filter on/off for the session by
# starting/stopping hyprsunset (nomarchy-nightlight, from nightlight.nix).
# Self-gated in the menu; guard here too in case it's invoked directly.
command -v nomarchy-nightlight >/dev/null 2>&1 \
&& exec nomarchy-nightlight toggle
notify-send "Night light" "Not available (nomarchy.nightlight off?)."; exit 0 ;;
snapshot)
# btrfs-assistant: snapshot browse / diff / restore / rollback over
# snapper, elevating via polkit. Shipped system-side with
@@ -219,6 +227,8 @@ let
row "Network" network-wireless
row "Bluetooth" bluetooth
row "Do Not Disturb" notification-disabled
systemctl --user cat hyprsunset.service >/dev/null 2>&1 \
&& row "Night light" weather-clear-night
command -v btrfs-assistant >/dev/null 2>&1 && row "Snapshots" timeshift
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
row "Power profile" preferences-system-power
@@ -229,6 +239,7 @@ let
*Network*) exec "$0" network ;;
*Bluetooth*) exec "$0" bluetooth ;;
*"Do Not Disturb"*) exec "$0" dnd ;;
*"Night light"*) exec "$0" nightlight ;;
*Snapshots*) exec "$0" snapshot ;;
*"Power profile"*) exec "$0" power-profile ;;
*Back*) exec "$0" ;;
@@ -256,7 +267,7 @@ let
esac ;;
*)
echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|capture|keybinds|ask|dnd|snapshot]" >&2
echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|capture|keybinds|ask|dnd|nightlight|snapshot]" >&2
exit 64 ;;
esac
'';

View File

@@ -77,7 +77,7 @@ let
modules-left = [ "hyprland/workspaces" "hyprland/window" ];
modules-center = [ "clock" ];
modules-right = [ "tray" "pulseaudio" "cpu" "memory" "custom/powerprofile" ]
modules-right = [ "tray" "pulseaudio" "cpu" "memory" "custom/powerprofile" "custom/nightlight" ]
++ lib.optional showLanguage "hyprland/language"
++ [ "battery" "custom/notification" ];
@@ -131,6 +131,16 @@ let
on-click = "nomarchy-powerprofile-cycle";
};
# Night-light (hyprsunset) toggle + state. Self-gates: hidden unless
# nomarchy.nightlight is enabled (the status helper prints nothing then).
# Click starts/stops the schedule; moon = on, sun = off.
"custom/nightlight" = {
exec = "nomarchy-nightlight status";
return-type = "json";
interval = 3;
on-click = "nomarchy-nightlight toggle";
};
# 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.
@@ -202,11 +212,14 @@ let
font-weight: bold;
}
#tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #language, #battery, #custom-notification {
#tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #custom-nightlight, #language, #battery, #custom-notification {
color: @subtext;
padding: 0 10px;
}
/* Night-light active warm tone, matching the filter it represents. */
#custom-nightlight.on { color: @warn; }
/* 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; }