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:
@@ -190,7 +190,7 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
|||||||
| `nomarchy.keyboard.variant` | `""` | XKB variant for the session |
|
| `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.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.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.hyprland.enable` | `true` | Nomarchy's Hyprland config |
|
||||||
| `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar |
|
| `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar |
|
||||||
| `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher |
|
| `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher |
|
||||||
|
|||||||
@@ -378,10 +378,18 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
warm at night, identity (no shift) by day. `modules/home/nightlight.nix`
|
warm at night, identity (no shift) by day. `modules/home/nightlight.nix`
|
||||||
drives the HM `services.hyprsunset` with two time-based profiles
|
drives the HM `services.hyprsunset` with two time-based profiles
|
||||||
(`sunrise` → identity, `sunset` → `temperature`), so hyprsunset handles the
|
(`sunrise` → identity, `sunset` → `temperature`), so hyprsunset handles the
|
||||||
schedule and the on-login state. Needs an on-hardware check that hyprsunset
|
schedule and the on-login state. Active-profile-at-session-start was
|
||||||
applies the active profile at session start. Remaining (optional): a menu +
|
confirmed on hardware (2026-06-18). ✓ **Menu + Waybar toggle:**
|
||||||
Waybar toggle to force it on/off, and geo (lat/long) auto sunset/sunrise
|
`nomarchy-nightlight` (in `nightlight.nix`, on PATH) starts/stops the
|
||||||
(would mean wlsunset, which schedules by location).
|
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):**
|
- **Keyboard layouts (per-device + switching):**
|
||||||
- ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices`
|
- ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices`
|
||||||
(`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland
|
(`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland
|
||||||
|
|||||||
@@ -5,14 +5,51 @@
|
|||||||
#
|
#
|
||||||
# The hyprsunset HM service module is provided by home-manager; this only
|
# The hyprsunset HM service module is provided by home-manager; this only
|
||||||
# configures it. Override anything with plain services.hyprsunset.* options.
|
# 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
|
let
|
||||||
cfg = config.nomarchy.nightlight;
|
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
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = {
|
||||||
services.hyprsunset = {
|
services.hyprsunset = lib.mkIf cfg.enable {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings.profile = [
|
settings.profile = [
|
||||||
# Daytime: identity = no colour change.
|
# Daytime: identity = no colour change.
|
||||||
@@ -21,5 +58,9 @@ in
|
|||||||
{ time = cfg.sunset; temperature = cfg.temperature; }
|
{ 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 ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,14 @@ let
|
|||||||
&& notify-send "Do Not Disturb off" "Notifications resumed."
|
&& notify-send "Do Not Disturb off" "Notifications resumed."
|
||||||
exit 0 ;;
|
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)
|
snapshot)
|
||||||
# btrfs-assistant: snapshot browse / diff / restore / rollback over
|
# btrfs-assistant: snapshot browse / diff / restore / rollback over
|
||||||
# snapper, elevating via polkit. Shipped system-side with
|
# snapper, elevating via polkit. Shipped system-side with
|
||||||
@@ -219,6 +227,8 @@ let
|
|||||||
row "Network" network-wireless
|
row "Network" network-wireless
|
||||||
row "Bluetooth" bluetooth
|
row "Bluetooth" bluetooth
|
||||||
row "Do Not Disturb" notification-disabled
|
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
|
command -v btrfs-assistant >/dev/null 2>&1 && row "Snapshots" timeshift
|
||||||
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
|
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
|
||||||
row "Power profile" preferences-system-power
|
row "Power profile" preferences-system-power
|
||||||
@@ -229,6 +239,7 @@ let
|
|||||||
*Network*) exec "$0" network ;;
|
*Network*) exec "$0" network ;;
|
||||||
*Bluetooth*) exec "$0" bluetooth ;;
|
*Bluetooth*) exec "$0" bluetooth ;;
|
||||||
*"Do Not Disturb"*) exec "$0" dnd ;;
|
*"Do Not Disturb"*) exec "$0" dnd ;;
|
||||||
|
*"Night light"*) exec "$0" nightlight ;;
|
||||||
*Snapshots*) exec "$0" snapshot ;;
|
*Snapshots*) exec "$0" snapshot ;;
|
||||||
*"Power profile"*) exec "$0" power-profile ;;
|
*"Power profile"*) exec "$0" power-profile ;;
|
||||||
*Back*) exec "$0" ;;
|
*Back*) exec "$0" ;;
|
||||||
@@ -256,7 +267,7 @@ let
|
|||||||
esac ;;
|
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 ;;
|
exit 64 ;;
|
||||||
esac
|
esac
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ let
|
|||||||
|
|
||||||
modules-left = [ "hyprland/workspaces" "hyprland/window" ];
|
modules-left = [ "hyprland/workspaces" "hyprland/window" ];
|
||||||
modules-center = [ "clock" ];
|
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"
|
++ lib.optional showLanguage "hyprland/language"
|
||||||
++ [ "battery" "custom/notification" ];
|
++ [ "battery" "custom/notification" ];
|
||||||
|
|
||||||
@@ -131,6 +131,16 @@ let
|
|||||||
on-click = "nomarchy-powerprofile-cycle";
|
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
|
# swaync notification bell + Do-Not-Disturb state. `-swb` streams JSON
|
||||||
# (text/tooltip/class) on every change, so it tracks count and DND with
|
# (text/tooltip/class) on every change, so it tracks count and DND with
|
||||||
# no polling. Left-click toggles the panel; right-click toggles DND.
|
# no polling. Left-click toggles the panel; right-click toggles DND.
|
||||||
@@ -202,11 +212,14 @@ let
|
|||||||
font-weight: bold;
|
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;
|
color: @subtext;
|
||||||
padding: 0 10px;
|
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 —
|
/* The power-profile speedometer glyph renders small in its em box —
|
||||||
size it up so it reads at a glance like the other indicators. */
|
size it up so it reads at a glance like the other indicators. */
|
||||||
#custom-powerprofile { font-size: ${toString (t.fonts.size + 3)}pt; }
|
#custom-powerprofile { font-size: ${toString (t.fonts.size + 3)}pt; }
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ window#waybar {
|
|||||||
#workspaces,
|
#workspaces,
|
||||||
#pulseaudio,
|
#pulseaudio,
|
||||||
#custom-powerprofile,
|
#custom-powerprofile,
|
||||||
|
#custom-nightlight,
|
||||||
#battery,
|
#battery,
|
||||||
#tray,
|
#tray,
|
||||||
#custom-notification,
|
#custom-notification,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
"modules-left": ["custom/launcher", "clock", "clock#date"],
|
"modules-left": ["custom/launcher", "clock", "clock#date"],
|
||||||
"modules-center": ["hyprland/workspaces"],
|
"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": {
|
"hyprland/workspaces": {
|
||||||
"disable-scroll": true,
|
"disable-scroll": true,
|
||||||
@@ -79,6 +79,13 @@
|
|||||||
"on-click": "nomarchy-powerprofile-cycle"
|
"on-click": "nomarchy-powerprofile-cycle"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"custom/nightlight": {
|
||||||
|
"return-type": "json",
|
||||||
|
"interval": 3,
|
||||||
|
"exec": "nomarchy-nightlight status",
|
||||||
|
"on-click": "nomarchy-nightlight toggle"
|
||||||
|
},
|
||||||
|
|
||||||
"custom/notification": {
|
"custom/notification": {
|
||||||
"format": "{icon}",
|
"format": "{icon}",
|
||||||
"return-type": "json",
|
"return-type": "json",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ window#waybar {
|
|||||||
#workspaces,
|
#workspaces,
|
||||||
#pulseaudio,
|
#pulseaudio,
|
||||||
#custom-powerprofile,
|
#custom-powerprofile,
|
||||||
|
#custom-nightlight,
|
||||||
#idle_inhibitor,
|
#idle_inhibitor,
|
||||||
#battery,
|
#battery,
|
||||||
#custom-notification,
|
#custom-notification,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
"modules-left": ["custom/nomarchy", "clock", "clock#date"],
|
"modules-left": ["custom/nomarchy", "clock", "clock#date"],
|
||||||
"modules-center": ["hyprland/workspaces"],
|
"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": {
|
"hyprland/workspaces": {
|
||||||
"disable-scroll": true,
|
"disable-scroll": true,
|
||||||
@@ -86,6 +86,13 @@
|
|||||||
"on-click": "nomarchy-powerprofile-cycle"
|
"on-click": "nomarchy-powerprofile-cycle"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"custom/nightlight": {
|
||||||
|
"return-type": "json",
|
||||||
|
"interval": 3,
|
||||||
|
"exec": "nomarchy-nightlight status",
|
||||||
|
"on-click": "nomarchy-nightlight toggle"
|
||||||
|
},
|
||||||
|
|
||||||
"custom/notification": {
|
"custom/notification": {
|
||||||
"format": "{icon}",
|
"format": "{icon}",
|
||||||
"return-type": "json",
|
"return-type": "json",
|
||||||
|
|||||||
Reference in New Issue
Block a user