feat(autotheme): fire at the configured sunrise/sunset, not a 15-min poll
All checks were successful
Check / eval (push) Successful in 4m14s

The timer's OnCalendar now bakes the configured times (falling back
like _hhmm_to_min on a hand-edited state), so the switch lands at the
configured minute instead of up to 15 minutes late. The original
poll's robustness case is covered without it: Persistent catches
transitions missed while powered off, systemd fires elapsed calendar
timers on resume, and OnStartupSec settles the theme after login.

Baked times mean a time edit must rebuild: the menu's Sunrise/Sunset
writes now run `auto --force` when enabled (one rebuild re-bakes the
timer and lands on the right theme, same as the enable flow). Day and
night slugs stay live reads — but with no poll to catch them up,
editing the slot currently on screen applies immediately via a plain
`auto`. New eval check `auto-theme-timer` asserts the bake
state-bridges-style: configured times reach OnCalendar, garbage falls
back to 07:00/20:00, disabled installs no unit. Docs + hardware-queue
entry updated to the new semantics.

V2: checks.auto-theme (KVM VM, passed) covers the auto engine the
timer ticks; checks.auto-theme-timer (eval, passed) covers the unit;
OnCalendar strings systemd-analyze-validated; the rebuilt menu script
passes bash -n with the new branches in place. V3 pending: live timer
firing at the exact configured minute on hardware (existing
"Auto-theme pair flip" queue entry, refreshed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 22:31:02 +01:00
parent ac24ac7c34
commit f658b0087e
6 changed files with 136 additions and 43 deletions

View File

@@ -962,13 +962,16 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
autotheme)
# Look & Feel Auto theme automatic day/night theme switch
# (settings.autoTheme, applied by the nomarchy-auto-theme timer from
# autotheme.nix). The enable flag GATES the timer's install, so
# turning it *on* must rebuild; day/night/times are read live by
# `nomarchy-state-sync auto`, so those writes are instant
# (--no-switch). Enabling writes the flag then `auto --force`, so a
# single rebuild both installs the timer and applies the right theme
# now. Disabling is instant: the flag flips and `auto` self-gates to
# a no-op, so the lingering timer does nothing until the next rebuild.
# autotheme.nix). The enable flag GATES the timer's install and the
# sunrise/sunset times are BAKED into its OnCalendar, so turning it
# *on* or editing a time must rebuild both run `auto --force`, so
# one rebuild re-bakes the timer AND lands on the right theme now.
# Day/night slugs are read live by `nomarchy-state-sync auto`, so
# those writes are instant (--no-switch); when the edited slot is the
# one on screen, a plain `auto` applies it right away (the timer only
# fires at the next transition there is no poll to catch it up).
# Disabling is instant: the flag flips and `auto` self-gates to a
# no-op, so the lingering timer does nothing until the next rebuild.
at_get() { nomarchy-state-sync get "settings.autoTheme.$1" 2>/dev/null; }
en=$(at_get enable); day=$(at_get day); night=$(at_get night)
sunrise=$(at_get sunrise); sunset=$(at_get sunset)
@@ -997,15 +1000,20 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
nomarchy-state-sync --quiet set settings.autoTheme.enable true --no-switch
notify-send "Auto theme" "On applying the theme for now"
exec nomarchy-state-sync auto --force ;; # one rebuild: install timer + apply
"Day theme"*)
slug=$( { nomarchy-state-sync list; printf '%s\n' "$BACK"; } | rofi_menu -p "Day theme") || exec "$0" autotheme
[ "$slug" != "$BACK" ] && [ -n "$slug" ] \
&& nomarchy-state-sync --quiet set settings.autoTheme.day "$slug" --no-switch
exec "$0" autotheme ;;
"Night theme"*)
slug=$( { nomarchy-state-sync list; printf '%s\n' "$BACK"; } | rofi_menu -p "Night theme") || exec "$0" autotheme
[ "$slug" != "$BACK" ] && [ -n "$slug" ] \
&& nomarchy-state-sync --quiet set settings.autoTheme.night "$slug" --no-switch
"Day theme"*|"Night theme"*)
key=day; label="Day theme"
case "$choice" in "Night theme"*) key=night; label="Night theme" ;; esac
slug=$( { nomarchy-state-sync list; printf '%s\n' "$BACK"; } | rofi_menu -p "$label") || exec "$0" autotheme
if [ "$slug" != "$BACK" ] && [ -n "$slug" ]; then
nomarchy-state-sync --quiet set "settings.autoTheme.$key" "$slug" --no-switch
# Edited the slot that is on screen right now? Apply it the
# timer won't fire again until the next sunrise/sunset.
if [ "$en" = true ] && [ "$(nomarchy-state-sync auto --which)" = "$slug" ] \
&& [ "$(nomarchy-state-sync get slug)" != "$slug" ]; then
notify-send "Auto theme" "$label: $slug applying"
exec nomarchy-state-sync auto
fi
fi
exec "$0" autotheme ;;
"Sunrise"*|"Sunset"*)
key=sunrise; label=Sunrise; cur="''${sunrise:-07:00}"
@@ -1014,7 +1022,14 @@ ${lib.concatStringsSep "\n" (lib.mapAttrsToList
case "$t" in
""|"$BACK") : ;;
[01][0-9]:[0-5][0-9]|2[0-3]:[0-5][0-9])
nomarchy-state-sync --quiet set "settings.autoTheme.$key" "$t" --no-switch ;;
nomarchy-state-sync --quiet set "settings.autoTheme.$key" "$t" --no-switch
# The time is baked into the timer's OnCalendar, so a change
# must rebuild; `auto --force` is that rebuild AND lands on
# the right theme for the new schedule in the same pass.
if [ "$en" = true ]; then
notify-send "Auto theme" "$label $t rebuilding the schedule"
exec nomarchy-state-sync auto --force
fi ;;
*) notify-send "Auto theme" "Ignored '$t' use HH:MM (24-hour)." ;;
esac
exec "$0" autotheme ;;