feat(nightlight): geo mode — lat/long-computed sunset via wlsunset
All checks were successful
Check / eval (push) Successful in 4m20s
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:
@@ -233,7 +233,7 @@ two tables below are split along exactly that line.
|
|||||||
| `nomarchy.keyboard.variant` | `""` | XKB variant for the session |
|
| `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.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.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.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.hyprland.enable` | `true` | Nomarchy's Hyprland config |
|
||||||
| `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar |
|
| `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar |
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ HARDWARE-QUEUE. Close #76 on PASS.
|
|||||||
- **Installer round 2** (ROADMAP § Installer): multi-disk BTRFS RAID,
|
- **Installer round 2** (ROADMAP § Installer): multi-disk BTRFS RAID,
|
||||||
impermanence, BIOS/legacy boot.
|
impermanence, BIOS/legacy boot.
|
||||||
- **Boot-from-snapshot**: a systemd-boot equivalent of grub-btrfs.
|
- **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).
|
- **MIPI/IPU software-ISP camera** support (no-UVC machines).
|
||||||
- **VPN exit-node richer display** (country/city) (optional).
|
- **VPN exit-node richer display** (country/city) (optional).
|
||||||
- **NixOS release bump → v2** `[human]`: deliberate, hand-edited, never
|
- **NixOS release bump → v2** `[human]`: deliberate, hand-edited, never
|
||||||
|
|||||||
@@ -224,6 +224,16 @@ the **T14s** (webcam case).
|
|||||||
hibernate — likely no swap is configured. See docs/MIGRATION.md →
|
hibernate — likely no swap is configured. See docs/MIGRATION.md →
|
||||||
Enabling hibernation."), **not** a silent no-op. (`swapon` after, if you
|
Enabling hibernation."), **not** a silent no-op. (`swapon` after, if you
|
||||||
swapoff'd for the test.)
|
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 dev box only
|
||||||
- [ ] **AMD runtime bits** — VA-API (`vainfo` → radeonsi), amd-pstate EPP
|
- [ ] **AMD runtime bits** — VA-API (`vainfo` → radeonsi), amd-pstate EPP
|
||||||
|
|||||||
@@ -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)
|
## 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
|
- **Task:** Bernardo: groom stale items and codify the check so they don't
|
||||||
recur; close the zram user-doc gap.
|
recur; close the zram user-doc gap.
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
file; next target: opt-in auto-commit of the flake on each mutation, owned
|
||||||
files only). Remaining (optional): geo (lat/long) auto
|
files only). The on-machine check PASSed 2026-07-04 (enable/disable on
|
||||||
sunset/sunrise (would mean wlsunset, which schedules by location). Pending an
|
hardware). **Geo mode shipped 2026-07-11:** set BOTH
|
||||||
on-machine check (first enable rebuilds + comes on; later toggles instant; off
|
`nomarchy.nightlight.latitude`/`.longitude` and wlsunset replaces
|
||||||
persists across reboot via ExecCondition; stopping hyprsunset restores gamma).
|
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`
|
- ✓ **Automatic timezone (location-following clock):** `nomarchy.system.autoTimezone`
|
||||||
(opt-in, off by default) — geoclue + `services.automatic-timezoned` drive
|
(opt-in, off by default) — geoclue + `services.automatic-timezoned` drive
|
||||||
`/etc/localtime` from your location, so travelling to another zone updates the
|
`/etc/localtime` from your location, so travelling to another zone updates the
|
||||||
|
|||||||
42
flake.nix
42
flake.nix
@@ -550,6 +550,48 @@
|
|||||||
in
|
in
|
||||||
assert ok; pkgs.runCommand "nomarchy-display-profiles" { } "touch $out";
|
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
|
# The low-battery watcher's crossing logic: one toast per downward
|
||||||
# crossing (normal at 25%, critical at 10%), re-armed by charging.
|
# crossing (normal at 25%, critical at 10%), re-armed by charging.
|
||||||
# test_power fakes a battery whose capacity/status we script;
|
# test_power fakes a battery whose capacity/status we script;
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
# schedule (temperature/sunrise/sunset) is tuned via nomarchy.nightlight.* in
|
# schedule (temperature/sunrise/sunset) is tuned via nomarchy.nightlight.* in
|
||||||
# home.nix and baked into the unit's time-based `profile`.
|
# 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
|
# Off by default and opt-in. Two git-tracked flags in the state file, both
|
||||||
# menu-written (no ~/.local/state):
|
# menu-written (no ~/.local/state):
|
||||||
# settings.nightlight.installed — does the hyprsunset unit exist? Sticky; the
|
# settings.nightlight.installed — does the hyprsunset unit exist? Sticky; the
|
||||||
@@ -18,12 +24,15 @@ let
|
|||||||
cfg = config.nomarchy.nightlight;
|
cfg = config.nomarchy.nightlight;
|
||||||
s = config.nomarchy.settings.nightlight;
|
s = config.nomarchy.settings.nightlight;
|
||||||
sync = lib.getExe config.nomarchy.package;
|
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
|
# 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.
|
# after the first enable). Baked at eval; only used when the live key is absent.
|
||||||
onDefault = lib.boolToString s.on;
|
onDefault = lib.boolToString s.on;
|
||||||
|
|
||||||
nomarchy-nightlight = pkgs.writeShellScriptBin "nomarchy-nightlight" ''
|
nomarchy-nightlight = pkgs.writeShellScriptBin "nomarchy-nightlight" ''
|
||||||
unit=hyprsunset.service
|
unit=${unit}
|
||||||
# Instant runtime on/off: write the in-flake state WITHOUT a rebuild.
|
# Instant runtime on/off: write the in-flake state WITHOUT a rebuild.
|
||||||
write_on() { ${sync} --quiet set settings.nightlight.on "$1" --no-switch; }
|
write_on() { ${sync} --quiet set settings.nightlight.on "$1" --no-switch; }
|
||||||
# First enable: mark the feature installed and REBUILD to create the unit
|
# 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.
|
# home.nix also works as a declarative opt-in.
|
||||||
nomarchy.nightlight.enable = lib.mkDefault s.installed;
|
nomarchy.nightlight.enable = lib.mkDefault s.installed;
|
||||||
|
|
||||||
services.hyprsunset = lib.mkIf cfg.enable {
|
services.hyprsunset = lib.mkIf (cfg.enable && !geo) {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings.profile = [
|
settings.profile = [
|
||||||
# Daytime: identity = no colour change.
|
# 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
|
# 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
|
# 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).
|
# 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";
|
lib.mkIf cfg.enable "${nomarchy-nightlight}/bin/nomarchy-nightlight should-start";
|
||||||
|
|
||||||
home.packages = [ nomarchy-nightlight ];
|
home.packages = [ nomarchy-nightlight ];
|
||||||
|
|||||||
@@ -192,8 +192,9 @@ in
|
|||||||
|
|
||||||
nightlight = {
|
nightlight = {
|
||||||
enable = lib.mkEnableOption ''
|
enable = lib.mkEnableOption ''
|
||||||
a scheduled blue-light filter (hyprsunset): warm at night, no shift
|
a scheduled blue-light filter (hyprsunset; wlsunset in geo mode):
|
||||||
by day. Opt-in; tune the temperature + sunrise/sunset below'';
|
warm at night, no shift by day. Opt-in; tune the temperature +
|
||||||
|
sunrise/sunset (or latitude/longitude) below'';
|
||||||
|
|
||||||
temperature = lib.mkOption {
|
temperature = lib.mkOption {
|
||||||
type = lib.types.int;
|
type = lib.types.int;
|
||||||
@@ -206,14 +207,31 @@ in
|
|||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "07:00";
|
default = "07:00";
|
||||||
example = "06:30";
|
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 {
|
sunset = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "20:00";
|
default = "20:00";
|
||||||
example = "21: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.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@
|
|||||||
# temperature = 4000; # night warmth in K — lower is warmer
|
# temperature = 4000; # night warmth in K — lower is warmer
|
||||||
# sunset = "20:00";
|
# sunset = "20:00";
|
||||||
# sunrise = "07: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
|
# nomarchy.updates = { # passive update-awareness indicator + notification
|
||||||
|
|||||||
Reference in New Issue
Block a user