diff --git a/README.md b/README.md index ff5c953..f032072 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/agent/BACKLOG.md b/agent/BACKLOG.md index 60c2b37..6ced128 100644 --- a/agent/BACKLOG.md +++ b/agent/BACKLOG.md @@ -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 diff --git a/agent/HARDWARE-QUEUE.md b/agent/HARDWARE-QUEUE.md index ee340cf..164f309 100644 --- a/agent/HARDWARE-QUEUE.md +++ b/agent/HARDWARE-QUEUE.md @@ -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 = ""; + longitude = ""; }` (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 diff --git a/agent/JOURNAL.md b/agent/JOURNAL.md index b0eaa9b..587c147 100644 --- a/agent/JOURNAL.md +++ b/agent/JOURNAL.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 91356f4..c744b70 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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 diff --git a/flake.nix b/flake.nix index 6e17d47..dd21d51 100644 --- a/flake.nix +++ b/flake.nix @@ -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; diff --git a/modules/home/nightlight.nix b/modules/home/nightlight.nix index 05d7d07..83794ba 100644 --- a/modules/home/nightlight.nix +++ b/modules/home/nightlight.nix @@ -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 ]; diff --git a/modules/home/options.nix b/modules/home/options.nix index 5c77555..4adbfa0 100644 --- a/modules/home/options.nix +++ b/modules/home/options.nix @@ -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."; }; }; diff --git a/templates/downstream/home.nix b/templates/downstream/home.nix index 16914dd..d7f1672 100644 --- a/templates/downstream/home.nix +++ b/templates/downstream/home.nix @@ -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