feat(nightlight): menu-driven, in-flake on/off (no ~/.local/state)
Night light is off by default and configured entirely through the menu,
writing into the downstream flake instead of ~/.local/state — so the
choice is git-tracked and reproducible across a clone+rebuild.
Two flags under a new settings.* section of the state file (exposed as
the nomarchy.settings option):
- settings.nightlight.installed — sticky, gates the hyprsunset unit;
nomarchy.nightlight.enable mkDefault-reads it. First enable from the
menu rebuilds to create the unit (the one accepted rebuild).
- settings.nightlight.on — runtime on/off, toggled instantly
(theme-sync set --no-switch + systemctl), no rebuild. An ExecCondition
reads the live flag at session start so an off survives reboot;
splitting it from the sticky flag means a later unrelated rebuild
never undoes an instant-off (no decay).
Drops the ~/.local/state/nomarchy/nightlight-off marker and the
ConditionPathExists hack. theme-sync set now skips the wallpaper
re-apply for non-wallpaper keys, and its rebuild notifications are
generic ("changes" not "theme").
First cut of the broader principle — any user-settable config gets a
menu writer that lands it in the flake; no state outside the checkout.
Docs (README §4, ROADMAP, template) updated to reflect it.
Eval- and round-trip-verified; on-hardware check pending.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,60 +1,74 @@
|
||||
# Night light — a scheduled blue-light filter via hyprsunset (Hyprland's own
|
||||
# gamma/temperature tool). Warm at night, identity (no shift) by day;
|
||||
# hyprsunset's time-based `profile` entries handle the schedule and pick the
|
||||
# right state on session start. Opt-in via nomarchy.nightlight.enable.
|
||||
# gamma/temperature tool). Warm at night, identity (no shift) by day; the
|
||||
# schedule (temperature/sunrise/sunset) is tuned via nomarchy.nightlight.* in
|
||||
# home.nix and baked into the unit's time-based `profile`.
|
||||
#
|
||||
# The hyprsunset HM service module is provided by home-manager; this only
|
||||
# configures it. Override anything with plain services.hyprsunset.* options.
|
||||
#
|
||||
# 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.
|
||||
# Off by default and opt-in. Two git-tracked flags in the state file, both
|
||||
# menu-written (no ~/.local/state):
|
||||
# settings.nightlight.installed — does the hyprsunset unit exist? Sticky; the
|
||||
# option mkDefault-reads it, so the FIRST enable from the menu rebuilds (to
|
||||
# create the unit) and an instant-off is never undone by a later rebuild.
|
||||
# settings.nightlight.on — runtime on/off. Toggled INSTANTLY (write + systemctl,
|
||||
# no rebuild); read by the unit's ExecCondition (should-start) at session
|
||||
# start so the choice survives logout/reboot via the *live* state, not the
|
||||
# eval-frozen store copy. A later rebuild bakes the same value (no divergence).
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.nomarchy.nightlight;
|
||||
s = config.nomarchy.settings.nightlight;
|
||||
sync = lib.getExe config.nomarchy.package;
|
||||
# Runtime-on default for when the `on` key hasn't been written yet (e.g. right
|
||||
# after the first enable). Baked at eval; only used when the live key is absent.
|
||||
onDefault = lib.boolToString s.on;
|
||||
|
||||
# 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
|
||||
# Persistent-off marker — mirrors the unit's ConditionPathExists below.
|
||||
# Present => night-light stays off across logout/reboot until toggled on.
|
||||
# Path is literal (not XDG_STATE_HOME) so it matches the unit's %h check.
|
||||
marker="$HOME/.local/state/nomarchy/nightlight-off"
|
||||
mark_off() { mkdir -p "$(dirname "$marker")"; : > "$marker"; }
|
||||
mark_on() { rm -f "$marker"; }
|
||||
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
|
||||
# Instant runtime on/off: write the in-flake state WITHOUT a rebuild.
|
||||
write_on() { ${sync} --quiet set settings.nightlight.on "$1" --no-switch; }
|
||||
# First enable: mark the feature installed and REBUILD to create the unit
|
||||
# (the one rebuild we accept; every toggle after is instant).
|
||||
install_feature() { ${sync} --quiet set settings.nightlight.installed true; }
|
||||
# Read the LIVE working-tree on/off (~/.nomarchy via $NOMARCHY_PATH), not the
|
||||
# store copy baked into this generation; fall back to the eval-time default
|
||||
# when absent. Normalise Python's True/False bool rendering.
|
||||
is_on() {
|
||||
v=$(${sync} get settings.nightlight.on 2>/dev/null) || v=${onDefault}
|
||||
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
|
||||
}
|
||||
installed() { systemctl --user cat "$unit" >/dev/null 2>&1; }
|
||||
start() { systemctl --user start "$unit" 2>/dev/null || true; }
|
||||
stop() { systemctl --user stop "$unit" 2>/dev/null || true; }
|
||||
case "''${1:-toggle}" in
|
||||
should-start) is_on ;; # ExecCondition gate (login/reboot)
|
||||
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) mark_on; systemctl --user start "$unit" ;;
|
||||
off) mark_off; systemctl --user stop "$unit" ;;
|
||||
# Waybar (polls every 3s): moon while running; print nothing otherwise so
|
||||
# the module self-hides — enable / re-enable from the System menu.
|
||||
systemctl --user is-active --quiet "$unit" \
|
||||
&& printf '{"text":"","tooltip":"Night light on — warm on schedule (click to disable)","class":"on"}\n'
|
||||
exit 0 ;;
|
||||
on)
|
||||
if installed; then write_on true; start; else install_feature; start; fi ;;
|
||||
off) write_on false; stop ;;
|
||||
toggle)
|
||||
if systemctl --user is-active --quiet "$unit"; then
|
||||
mark_off; systemctl --user stop "$unit"
|
||||
notify-send "Night light off" "Screen back to true colours (stays off until re-enabled)."
|
||||
write_on false; stop # on -> off (instant)
|
||||
elif installed; then
|
||||
write_on true; start # installed, off -> on (instant)
|
||||
else
|
||||
mark_on; systemctl --user start "$unit"
|
||||
notify-send "Night light on" "Warm filter follows your schedule."
|
||||
install_feature; start # first enable (rebuilds)
|
||||
fi ;;
|
||||
*) echo "usage: nomarchy-nightlight [toggle|status|on|off]" >&2; exit 64 ;;
|
||||
*) echo "usage: nomarchy-nightlight [toggle|status|on|off|should-start]" >&2; exit 64 ;;
|
||||
esac
|
||||
'';
|
||||
in
|
||||
{
|
||||
config = {
|
||||
# Unit presence tracks the sticky `installed` flag the menu writes (first
|
||||
# enable rebuilds). mkDefault so a hand-set nomarchy.nightlight.enable in
|
||||
# home.nix also works as a declarative opt-in.
|
||||
nomarchy.nightlight.enable = lib.mkDefault s.installed;
|
||||
|
||||
services.hyprsunset = lib.mkIf cfg.enable {
|
||||
enable = true;
|
||||
settings.profile = [
|
||||
@@ -65,15 +79,12 @@ in
|
||||
];
|
||||
};
|
||||
|
||||
# Persistent off: a manual `off` drops ~/.local/state/nomarchy/nightlight-off,
|
||||
# and this Condition makes the unit refuse to (re)start while it exists — so
|
||||
# an off survives logout/reboot. `nomarchy-nightlight on` removes it first
|
||||
# (a failed condition would otherwise no-op the start). %h = $HOME.
|
||||
systemd.user.services.hyprsunset.Unit.ConditionPathExists =
|
||||
lib.mkIf cfg.enable "!%h/.local/state/nomarchy/nightlight-off";
|
||||
# Gate the unit on the LIVE on/off state at start time (login/reboot), not
|
||||
# at eval time — so a menu toggle (written without a rebuild) is honoured on
|
||||
# the next session. A failed condition skips the unit (inactive, not failed).
|
||||
systemd.user.services.hyprsunset.Service.ExecCondition =
|
||||
lib.mkIf cfg.enable "${nomarchy-nightlight}/bin/nomarchy-nightlight should-start";
|
||||
|
||||
# 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 ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -255,6 +255,17 @@ in
|
||||
description = "The parsed theme state (stateFile merged over defaults).";
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
readOnly = true;
|
||||
description = ''
|
||||
Parsed feature settings — the `settings` section of the state file,
|
||||
what the menu/Waybar toggles write (e.g. settings.nightlight.enable).
|
||||
Feature options mkDefault-read from here, so a menu toggle lands in the
|
||||
in-flake state (git-tracked, reproducible) rather than ~/.local/state.
|
||||
'';
|
||||
};
|
||||
|
||||
lib = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
readOnly = true;
|
||||
|
||||
@@ -370,8 +370,10 @@ ${themeRows}
|
||||
&& row "Audio" audio-volume-high
|
||||
command -v system-config-printer >/dev/null 2>&1 && row "Printers" printer
|
||||
row "Do Not Disturb" notification-disabled
|
||||
systemctl --user cat hyprsunset.service >/dev/null 2>&1 \
|
||||
&& row "Night light" weather-clear-night
|
||||
if systemctl --user is-active --quiet hyprsunset.service 2>/dev/null
|
||||
then row "Night light (on)" weather-clear-night
|
||||
else row "Night light (off)" weather-clear-night
|
||||
fi
|
||||
command -v nomarchy-snapshots >/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
|
||||
|
||||
@@ -54,6 +54,12 @@ let
|
||||
# tone for kanagawa/summer-*). Each preset declares its own so a theme
|
||||
# switch always replaces it (deep_merge would otherwise leave it stuck).
|
||||
border = { active = "accent"; inactive = "overlay"; };
|
||||
|
||||
# Non-appearance feature settings the menu writes into this same in-flake
|
||||
# state. nomarchy.nightlight: `installed` (sticky — gates the unit, so the
|
||||
# first enable rebuilds) and `on` (instant runtime on/off). Defaulted so a
|
||||
# sparse/older state file still evaluates; nomarchy.settings exposes them.
|
||||
settings.nightlight = { installed = false; on = true; };
|
||||
};
|
||||
|
||||
parsed = lib.recursiveUpdate defaults themeState;
|
||||
@@ -78,6 +84,11 @@ in
|
||||
config = {
|
||||
nomarchy.theme = parsed // { inherit iconTheme border; };
|
||||
|
||||
# Feature toggles the menu writes (settings.nightlight.enable, …), exposed
|
||||
# alongside the appearance state. Feature modules mkDefault-read from here
|
||||
# so a menu toggle lands in the flake instead of in ~/.local/state.
|
||||
nomarchy.settings = parsed.settings;
|
||||
|
||||
nomarchy.lib = {
|
||||
# "#7aa2f7" -> "rgb(7aa2f7)" (Hyprland color syntax)
|
||||
rgb = c: "rgb(${lib.removePrefix "#" c})";
|
||||
|
||||
@@ -131,9 +131,11 @@ 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.
|
||||
# Night-light (hyprsunset) indicator. Self-gates: the moon shows only while
|
||||
# the schedule runs; otherwise the status helper prints nothing => hidden
|
||||
# (enable / re-enable from the System menu). Click toggles instantly — writes
|
||||
# the in-flake on/off (settings.nightlight.on, no rebuild) and flips the unit
|
||||
# with systemctl, so the choice lands in the flake and survives reboot.
|
||||
"custom/nightlight" = {
|
||||
exec = "nomarchy-nightlight status";
|
||||
return-type = "json";
|
||||
|
||||
Reference in New Issue
Block a user