feat(nightlight): geo mode — lat/long-computed sunset via wlsunset
All checks were successful
Check / eval (push) Successful in 4m20s

Set BOTH nomarchy.nightlight.latitude/.longitude and the night light
switches backing unit: wlsunset computes sunrise/sunset from the
location daily (the fixed .sunrise/.sunset are ignored;
.temperature feeds the night temp). Everything user-facing is
unchanged — same nomarchy-nightlight toggle script, Waybar moon, and
live-state ExecCondition gate; the unit name is a single let-binding
so all three follow together. Declarative-only on purpose: coordinates
are machine config like the keyboard layout, so home.nix (commented
template example), not a menu writer.

checks.nightlight-geo guards the wiring at pure eval: rendered
wlsunset ExecStart carries the coords + night temperature, the
ExecCondition gate landed on the swapped unit, hyprsunset stays off in
geo mode.

Verification: V1 — nix flake check --no-build exit 0 (evals the new
asserts + option-docs sync); built downstream-template-home (default
hyprsunset path unregressed). V3 pending: visible warm shift at the
location's night + instant toggle/off-persistence on the swapped unit
(HARDWARE-QUEUE § Any machine).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-07-11 09:30:52 +01:00
parent 4c656b1e73
commit 4ebd6770ec
9 changed files with 127 additions and 13 deletions

View File

@@ -233,7 +233,7 @@ two tables below are split along exactly that line.
| `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; off by default — enable it from the menu (System Night light; the first enable rebuilds), then toggle on/off instantly from the menu or the Waybar moon indicator (writes `settings.nightlight.on` in your flake, no rebuild) and it survives reboot, so it stays reproducible |
| `nomarchy.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day; set BOTH `.latitude`/`.longitude` for **geo mode** (wlsunset computes sunrise/sunset from your location; the fixed times are ignored); off by default — enable it from the menu (System Night light; the first enable rebuilds), then toggle on/off instantly from the menu or the Waybar moon indicator (writes `settings.nightlight.on` in your flake, no rebuild) and it survives reboot, so it stays reproducible |
| `nomarchy.updates.enable` | `false` | Passive update awareness: a background check (`.interval`, default daily) that flags when flake inputs (nixpkgs, the Nomarchy input, …) are behind upstream — and, with Flatpak on, when apps have updates (`.flatpak`) — via a Waybar indicator + notification. Never changes anything; click the indicator to run the upgrade flow |
| `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config |
| `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar |

View File

@@ -64,7 +64,6 @@ HARDWARE-QUEUE. Close #76 on PASS.
- **Installer round 2** (ROADMAP § Installer): multi-disk BTRFS RAID,
impermanence, BIOS/legacy boot.
- **Boot-from-snapshot**: a systemd-boot equivalent of grub-btrfs.
- **Night-light geo mode**: lat/long auto sunset/sunrise (means wlsunset).
- **MIPI/IPU software-ISP camera** support (no-UVC machines).
- **VPN exit-node richer display** (country/city) (optional).
- **NixOS release bump → v2** `[human]`: deliberate, hand-edited, never

View File

@@ -224,6 +224,16 @@ the **T14s** (webcam case).
hibernate — likely no swap is configured. See docs/MIGRATION.md →
Enabling hibernation."), **not** a silent no-op. (`swapon` after, if you
swapoff'd for the test.)
- [ ] **Night-light geo mode (2026-07-11)** — in `home.nix` set
`nomarchy.nightlight = { enable = true; latitude = "<lat>";
longitude = "<long>"; }` (your real coordinates), `nomarchy-home`,
relogin. Then: `systemctl --user status wlsunset` is running (and
`hyprsunset` is NOT); after local sunset the screen visibly warms
(or test by setting coordinates where it's currently night); the
Waybar moon + menu toggle still flip it instantly and an *off*
survives relogin (the ExecCondition gate on the swapped unit —
the eval check proves the wiring, not the runtime gate). **Pass** =
warm shift at the location's night + toggle/persistence intact.
## AMD dev box only
- [ ] **AMD runtime bits** — VA-API (`vainfo` → radeonsi), amd-pstate EPP

View File

@@ -19,6 +19,24 @@ Template:
---
## 2026-07-11 — Night-light geo mode (lat/long → wlsunset)
- **Task:** BACKLOG LATER "Night-light geo mode" (human-picked).
- **Did:** `nomarchy.nightlight.latitude`/`.longitude` (both set = geo mode):
`services.wlsunset` (location-computed sunrise/sunset, `.temperature`
night temp) replaces hyprsunset; the toggle script's `unit=` and the
live-state ExecCondition follow via one `unit` let-binding. Template
example lines, README row, ROADMAP ✓. New `checks.nightlight-geo`:
pure-eval asserts on the rendered units (coords + temp in ExecStart,
ExecCondition present, hyprsunset off). Declarative-only (no menu writer —
coords are machine config like keyboard layout).
- **Verified:** V1 — `nix flake check --no-build` exit 0 (evals the new
asserts + option-docs); built `downstream-template-home` (default
hyprsunset path unregressed) + the check drv. Caught en route: HM renders
`ExecStart` as a list (`toString` needed in eval asserts).
- **Pending:** V3 queued (HARDWARE-QUEUE § Any machine): visible warm shift
at location night + toggle/persistence on the swapped unit.
- **Next suggestion:** wallpapers artifact split (LATER, decided).
## 2026-07-11 — Sync-sweep rule + grooming + zram user docs (human-requested)
- **Task:** Bernardo: groom stale items and codify the check so they don't
recur; close the zram user-doc gap.

View File

@@ -721,10 +721,16 @@ Design/decision records and a running log of shipped work (items marked
outside the checkout** (Phase 1 night-light + Phase 2 the per-device
keyboard-layout memory below are done — both now in the git-tracked state
file; next target: opt-in auto-commit of the flake on each mutation, owned
files only). Remaining (optional): geo (lat/long) auto
sunset/sunrise (would mean wlsunset, which schedules by location). Pending an
on-machine check (first enable rebuilds + comes on; later toggles instant; off
persists across reboot via ExecCondition; stopping hyprsunset restores gamma).
files only). The on-machine check PASSed 2026-07-04 (enable/disable on
hardware). **Geo mode shipped 2026-07-11:** set BOTH
`nomarchy.nightlight.latitude`/`.longitude` and wlsunset replaces
hyprsunset — sunrise/sunset computed from the location daily, the fixed
times ignored; same toggle script, moon indicator, and live-state
`ExecCondition` gate (the backing unit name is a single let-binding).
Declarative-only on purpose (coords are hardware-independent machine
config, like the keyboard layout — no menu writer). Guarded by
`checks.nightlight-geo` (pure eval over the rendered units); the visible
warm shift is hardware-tier, queued.
- ✓ **Automatic timezone (location-following clock):** `nomarchy.system.autoTimezone`
(opt-in, off by default) — geoclue + `services.automatic-timezoned` drive
`/etc/localtime` from your location, so travelling to another zone updates the

View File

@@ -550,6 +550,48 @@
in
assert ok; pkgs.runCommand "nomarchy-display-profiles" { } "touch $out";
# Night-light geo mode: with BOTH lat/long set the backing unit
# flips hyprsunset → wlsunset (location-computed sunrise/sunset)
# and the live-state ExecCondition gate survives the swap (the
# toggle script's unit= comes from the same let-binding, so the
# gate landing on wlsunset covers the selection logic). Pure
# eval over the rendered HM units — nothing is built. The
# visible warm shift itself stays hardware-tier (HARDWARE-QUEUE),
# exactly like the original night-light full cycle.
nightlight-geo =
let
inherit (nixpkgs) lib;
hm = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
modules = [
self.homeModules.nomarchy
{
nomarchy.stateFile = ./templates/downstream/theme-state.json;
nomarchy.nightlight = {
enable = true;
latitude = "52.52";
longitude = "13.40";
};
home = { username = "geo"; homeDirectory = "/home/geo"; };
}
];
};
units = hm.config.systemd.user.services;
exec = toString units.wlsunset.Service.ExecStart;
cond = toString units.wlsunset.Service.ExecCondition;
expect = c: msg: lib.assertMsg c "nightlight-geo: ${msg}";
ok =
expect (lib.hasInfix "52.52" exec && lib.hasInfix "13.40" exec)
"wlsunset ExecStart lacks the configured coordinates: ${exec}"
&& expect (lib.hasInfix "4000" exec)
"wlsunset ExecStart lacks the night temperature: ${exec}"
&& expect (lib.hasInfix "should-start" cond)
"wlsunset lost the live-state ExecCondition gate"
&& expect (!(hm.config.services.hyprsunset.enable or false))
"geo mode must not also run hyprsunset";
in
assert ok; pkgs.runCommand "nomarchy-nightlight-geo" { } "touch $out";
# The low-battery watcher's crossing logic: one toast per downward
# crossing (normal at 25%, critical at 10%), re-armed by charging.
# test_power fakes a battery whose capacity/status we script;

View File

@@ -3,6 +3,12 @@
# schedule (temperature/sunrise/sunset) is tuned via nomarchy.nightlight.* in
# home.nix and baked into the unit's time-based `profile`.
#
# Geo mode: when BOTH nomarchy.nightlight.latitude and .longitude are set,
# wlsunset replaces hyprsunset — it computes sunrise/sunset from the
# location itself (the fixed .sunrise/.sunset times are ignored). Same
# toggle script, same Waybar moon, same live-state ExecCondition gate;
# only the unit underneath changes.
#
# 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
@@ -18,12 +24,15 @@ let
cfg = config.nomarchy.nightlight;
s = config.nomarchy.settings.nightlight;
sync = lib.getExe config.nomarchy.package;
# Geo mode flips the backing unit; everything user-facing stays the same.
geo = cfg.latitude != null && cfg.longitude != null;
unit = if geo then "wlsunset.service" else "hyprsunset.service";
# 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;
nomarchy-nightlight = pkgs.writeShellScriptBin "nomarchy-nightlight" ''
unit=hyprsunset.service
unit=${unit}
# 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
@@ -69,7 +78,7 @@ in
# home.nix also works as a declarative opt-in.
nomarchy.nightlight.enable = lib.mkDefault s.installed;
services.hyprsunset = lib.mkIf cfg.enable {
services.hyprsunset = lib.mkIf (cfg.enable && !geo) {
enable = true;
settings.profile = [
# Daytime: identity = no colour change.
@@ -79,10 +88,19 @@ in
];
};
# Geo mode: wlsunset owns the schedule — it recomputes sunrise/sunset
# from the coordinates daily (no fixed profile to bake).
services.wlsunset = lib.mkIf (cfg.enable && geo) {
enable = true;
latitude = cfg.latitude;
longitude = cfg.longitude;
temperature.night = cfg.temperature;
};
# 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 =
systemd.user.services.${lib.removeSuffix ".service" unit}.Service.ExecCondition =
lib.mkIf cfg.enable "${nomarchy-nightlight}/bin/nomarchy-nightlight should-start";
home.packages = [ nomarchy-nightlight ];

View File

@@ -192,8 +192,9 @@ in
nightlight = {
enable = lib.mkEnableOption ''
a scheduled blue-light filter (hyprsunset): warm at night, no shift
by day. Opt-in; tune the temperature + sunrise/sunset below'';
a scheduled blue-light filter (hyprsunset; wlsunset in geo mode):
warm at night, no shift by day. Opt-in; tune the temperature +
sunrise/sunset (or latitude/longitude) below'';
temperature = lib.mkOption {
type = lib.types.int;
@@ -206,14 +207,31 @@ in
type = lib.types.str;
default = "07:00";
example = "06:30";
description = "Time (HH:MM) the filter turns OFF daytime, no colour shift.";
description = "Time (HH:MM) the filter turns OFF daytime, no colour shift. Ignored in geo mode.";
};
sunset = lib.mkOption {
type = lib.types.str;
default = "20:00";
example = "21:00";
description = "Time (HH:MM) the filter turns ON warm.";
description = "Time (HH:MM) the filter turns ON warm. Ignored in geo mode.";
};
latitude = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "52.52";
description = ''
Geo mode: set BOTH latitude and longitude and sunrise/sunset are
computed from your location every day (wlsunset replaces
hyprsunset; the fixed .sunrise/.sunset times are ignored).'';
};
longitude = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "13.40";
description = "Geo mode longitude set together with latitude.";
};
};

View File

@@ -32,6 +32,9 @@
# temperature = 4000; # night warmth in K — lower is warmer
# sunset = "20:00";
# sunrise = "07:00";
# latitude = "52.52"; # geo mode: set BOTH lat/long and sunrise/
# longitude = "13.40"; # sunset follow your location (wlsunset);
# # the fixed times above are then ignored
# };
#
# nomarchy.updates = { # passive update-awareness indicator + notification