feat(autotheme): fire at the configured sunrise/sunset, not a 15-min poll
All checks were successful
Check / eval (push) Successful in 4m14s
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:
@@ -1,18 +1,37 @@
|
||||
# Auto time-of-day theme switch (BACKLOG #79, VISION § D). A user timer
|
||||
# periodically runs `nomarchy-state-sync auto`, which reads
|
||||
# settings.autoTheme = { enable, day, night, sunrise, sunset } from the
|
||||
# state file and applies the day or night preset for the current clock —
|
||||
# through the SAME one engine as a manual `apply` (no second pipeline).
|
||||
# runs `nomarchy-state-sync auto` at the configured sunrise and sunset,
|
||||
# which reads settings.autoTheme = { enable, day, night, sunrise, sunset }
|
||||
# from the state file and applies the day or night preset for the current
|
||||
# clock — through the SAME one engine as a manual `apply` (no second
|
||||
# pipeline).
|
||||
#
|
||||
# The command self-gates (no-op when disabled) and is idempotent (it only
|
||||
# rebuilds when the active theme actually needs to change), so the timer
|
||||
# can tick freely. Install is gated on the state flag (like nomarchy.updates
|
||||
# gates on its enable), so a machine not using the feature carries no timer;
|
||||
# enabling it from the menu (slice 3) writes the flag + rebuilds, which is
|
||||
# what brings the timer into being.
|
||||
# The trigger times are BAKED into OnCalendar at rebuild (originally this
|
||||
# was a 15-min poll; exact times won on wasted wakeups — Bernardo
|
||||
# 2026-07-18), so a time edit must rebuild: the menu's Sunrise/Sunset
|
||||
# writes run `auto --force`, whose apply is that rebuild. Missed
|
||||
# transitions are covered without polling: Persistent=true catches ones
|
||||
# that pass while powered off, systemd fires elapsed OnCalendar timers on
|
||||
# resume from suspend, and OnStartupSec settles the theme just after
|
||||
# login. The command self-gates (no-op when disabled) and is idempotent
|
||||
# (it only rebuilds when the active theme actually needs to change), so
|
||||
# every extra firing is cheap.
|
||||
#
|
||||
# Install is gated on the state flag (like nomarchy.updates gates on its
|
||||
# enable), so a machine not using the feature carries no timer; enabling
|
||||
# it from the menu (slice 3) writes the flag + rebuilds, which is what
|
||||
# brings the timer into being.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
lib.mkIf (config.nomarchy.settings.autoTheme.enable or false) {
|
||||
let
|
||||
at = config.nomarchy.settings.autoTheme or { };
|
||||
# "HH:MM" or the fallback — mirrors _hhmm_to_min in nomarchy-state-sync,
|
||||
# so a hand-edited state can't bake an OnCalendar systemd rejects.
|
||||
hhmm = v: fallback:
|
||||
if builtins.isString v
|
||||
&& builtins.match "([01][0-9]|2[0-3]):[0-5][0-9]" v != null
|
||||
then v else fallback;
|
||||
in
|
||||
lib.mkIf (at.enable or false) {
|
||||
systemd.user.services.nomarchy-auto-theme = {
|
||||
Unit.Description = "Apply the day/night theme for the current time";
|
||||
Service = {
|
||||
@@ -25,10 +44,13 @@ lib.mkIf (config.nomarchy.settings.autoTheme.enable or false) {
|
||||
};
|
||||
|
||||
systemd.user.timers.nomarchy-auto-theme = {
|
||||
Unit.Description = "Periodic day/night theme check";
|
||||
Unit.Description = "Day/night theme switch at sunrise and sunset";
|
||||
Timer = {
|
||||
OnStartupSec = "1min"; # settle on the right theme shortly after login
|
||||
OnCalendar = "*:0/15"; # re-check every 15 min; auto no-ops if unchanged
|
||||
OnCalendar = [
|
||||
"*-*-* ${hhmm (at.sunrise or null) "07:00"}:00" # sunrise → day
|
||||
"*-*-* ${hhmm (at.sunset or null) "20:00"}:00" # sunset → night
|
||||
];
|
||||
Persistent = true; # catch a transition missed while powered off
|
||||
};
|
||||
Install.WantedBy = [ "timers.target" ];
|
||||
|
||||
@@ -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 ;;
|
||||
|
||||
Reference in New Issue
Block a user