From 5c43a932856e2df0304a0076ed8d051750426b28 Mon Sep 17 00:00:00 2001 From: Bernardo Magri Date: Thu, 18 Jun 2026 22:23:12 +0100 Subject: [PATCH] feat(nightlight): menu + Waybar on/off toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 2 +- docs/ROADMAP.md | 16 ++++++++--- modules/home/nightlight.nix | 47 ++++++++++++++++++++++++++++++-- modules/home/rofi.nix | 13 ++++++++- modules/home/waybar.nix | 17 ++++++++++-- themes/summer-day/waybar.css | 1 + themes/summer-day/waybar.jsonc | 9 +++++- themes/summer-night/waybar.css | 1 + themes/summer-night/waybar.jsonc | 9 +++++- 9 files changed, 102 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1a47ed3..ecca9f8 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**. | `nomarchy.keyboard.variant` | `""` | XKB variant for the session | | `nomarchy.keyboard.devices` | `{}` | Per-device layout overrides (Hyprland `device` blocks keyed by `hyprctl devices` name) — e.g. an external keyboard with its own layout/variant | | `nomarchy.keyboard.layouts` | `[]` | Extra candidate layouts; when set, a watcher prompts (rofi) for a layout on a newly-connected keyboard and remembers it per-device | -| `nomarchy.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day | +| `nomarchy.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day; toggle it on/off from the menu (System › Night light) or the Waybar moon/sun indicator | | `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config | | `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar | | `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index df77081..0c9612d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -378,10 +378,18 @@ how to override it. Items marked ✓ are shipped. warm at night, identity (no shift) by day. `modules/home/nightlight.nix` drives the HM `services.hyprsunset` with two time-based profiles (`sunrise` → identity, `sunset` → `temperature`), so hyprsunset handles the - schedule and the on-login state. Needs an on-hardware check that hyprsunset - applies the active profile at session start. Remaining (optional): a menu + - Waybar toggle to force it on/off, and geo (lat/long) auto sunset/sunrise - (would mean wlsunset, which schedules by location). + schedule and the on-login state. Active-profile-at-session-start was + confirmed on hardware (2026-06-18). ✓ **Menu + Waybar toggle:** + `nomarchy-nightlight` (in `nightlight.nix`, on PATH) starts/stops the + hyprsunset *service* — `systemctl --user is-active` is the single source of + truth, the screen restores to true colours when stopped, and it forces the + filter off for colour-sensitive work and back on without touching config. A + `nomarchy-menu nightlight` entry (System submenu, self-gated on the unit + existing) and a self-gating Waybar `custom/nightlight` indicator (moon = on, + sun = off; in the generated bar and both summer whole-swaps) drive it. + Remaining (optional): geo (lat/long) auto sunset/sunrise (would mean + wlsunset, which schedules by location). Pending an on-machine check that + stopping hyprsunset cleanly restores the gamma. - **Keyboard layouts (per-device + switching):** - ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices` (`{ "" = { layout; variant; }; }`) generates Hyprland diff --git a/modules/home/nightlight.nix b/modules/home/nightlight.nix index ffa0946..db4aca0 100644 --- a/modules/home/nightlight.nix +++ b/modules/home/nightlight.nix @@ -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 ]; }; } diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index af676cb..e87f309 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -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 ''; diff --git a/modules/home/waybar.nix b/modules/home/waybar.nix index 6b360f1..82e9292 100644 --- a/modules/home/waybar.nix +++ b/modules/home/waybar.nix @@ -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; } diff --git a/themes/summer-day/waybar.css b/themes/summer-day/waybar.css index 4c8f7ed..af01c64 100755 --- a/themes/summer-day/waybar.css +++ b/themes/summer-day/waybar.css @@ -48,6 +48,7 @@ window#waybar { #workspaces, #pulseaudio, #custom-powerprofile, +#custom-nightlight, #battery, #tray, #custom-notification, diff --git a/themes/summer-day/waybar.jsonc b/themes/summer-day/waybar.jsonc index 71e99b0..6bce4a2 100644 --- a/themes/summer-day/waybar.jsonc +++ b/themes/summer-day/waybar.jsonc @@ -11,7 +11,7 @@ "modules-left": ["custom/launcher", "clock", "clock#date"], "modules-center": ["hyprland/workspaces"], - "modules-right": ["pulseaudio", "custom/powerprofile", "battery", "tray", "custom/notification", "custom/powermenu"], + "modules-right": ["pulseaudio", "custom/powerprofile", "custom/nightlight", "battery", "tray", "custom/notification", "custom/powermenu"], "hyprland/workspaces": { "disable-scroll": true, @@ -79,6 +79,13 @@ "on-click": "nomarchy-powerprofile-cycle" }, + "custom/nightlight": { + "return-type": "json", + "interval": 3, + "exec": "nomarchy-nightlight status", + "on-click": "nomarchy-nightlight toggle" + }, + "custom/notification": { "format": "{icon}", "return-type": "json", diff --git a/themes/summer-night/waybar.css b/themes/summer-night/waybar.css index 81999e6..23e3ffb 100644 --- a/themes/summer-night/waybar.css +++ b/themes/summer-night/waybar.css @@ -54,6 +54,7 @@ window#waybar { #workspaces, #pulseaudio, #custom-powerprofile, +#custom-nightlight, #idle_inhibitor, #battery, #custom-notification, diff --git a/themes/summer-night/waybar.jsonc b/themes/summer-night/waybar.jsonc index a30a535..c4996f1 100644 --- a/themes/summer-night/waybar.jsonc +++ b/themes/summer-night/waybar.jsonc @@ -11,7 +11,7 @@ "modules-left": ["custom/nomarchy", "clock", "clock#date"], "modules-center": ["hyprland/workspaces"], - "modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "battery", "tray", "custom/notification", "custom/powermenu"], + "modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "battery", "tray", "custom/notification", "custom/powermenu"], "hyprland/workspaces": { "disable-scroll": true, @@ -86,6 +86,13 @@ "on-click": "nomarchy-powerprofile-cycle" }, + "custom/nightlight": { + "return-type": "json", + "interval": 3, + "exec": "nomarchy-nightlight status", + "on-click": "nomarchy-nightlight toggle" + }, + "custom/notification": { "format": "{icon}", "return-type": "json",