# Night light — a scheduled blue-light filter via hyprsunset (Hyprland's own # gamma/temperature tool). Warm at night, identity (no shift) by day; the # 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 # option mkDefault-reads it, so the FIRST enable from the menu rebuilds (to # create the unit) and an instant-off is never undone by a later rebuild. # settings.nightlight.on — runtime on/off. Toggled INSTANTLY (write + systemctl, # no rebuild); read by the unit's ExecCondition (should-start) at session # start so the choice survives logout/reboot via the *live* state, not the # eval-frozen store copy. A later rebuild bakes the same value (no divergence). { config, lib, pkgs, ... }: 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=${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 # (the one rebuild we accept; every toggle after is instant). install_feature() { ${sync} --quiet set settings.nightlight.installed true; } # Read the LIVE working-tree on/off (~/.nomarchy via $NOMARCHY_PATH), not the # store copy baked into this generation; fall back to the eval-time default # when absent. Normalise Python's True/False bool rendering. is_on() { v=$(${sync} get settings.nightlight.on 2>/dev/null) || v=${onDefault} case "$v" in true|True) return 0 ;; *) return 1 ;; esac } # LoadState=loaded means a real unit (not the empty-file mask HM/packages # leave behind for unused hyprsunset). `systemctl cat` alone succeeds on # a masked unit and wrongly reported "installed" → silent no-op start. installed() { [ "$(systemctl --user show -p LoadState --value "$unit" 2>/dev/null)" = loaded ] } active() { systemctl --user is-active --quiet "$unit" 2>/dev/null; } start() { systemctl --user daemon-reload 2>/dev/null || true systemctl --user unmask "$unit" 2>/dev/null || true systemctl --user start "$unit" 2>/dev/null || true } stop() { systemctl --user stop "$unit" 2>/dev/null || true; } case "''${1:-toggle}" in should-start) is_on ;; # ExecCondition gate (login/reboot) status) # Waybar (polls every 3s): moon while running; print nothing otherwise so # the module self-hides — enable / re-enable from Look & Feel. # `active` alone is the truth for the moon; menu labels use the same. active \ && printf '{"text":"󰖔","tooltip":"Night light on — ${if geo then "follows your location" else "warm on schedule"} (click to disable)","class":"on"}\n' exit 0 ;; # Plain on/off string for menus (hyprsunset OR wlsunset; not hard-coded). is-active) if active; then echo on; else echo off; fi ;; on) if installed; then write_on true; start; else install_feature; start; fi ;; off) write_on false; stop ;; toggle) if active; then write_on false; stop # on -> off (instant) elif installed; then write_on true; start # installed, off -> on (instant) else install_feature; start # first enable (rebuilds) fi ;; *) echo "usage: nomarchy-nightlight [toggle|status|on|off|should-start|is-active]" >&2; exit 64 ;; esac ''; in { config = { # Unit presence tracks the sticky `installed` flag the menu writes (first # enable rebuilds). mkDefault so a hand-set nomarchy.nightlight.enable in # home.nix also works as a declarative opt-in. nomarchy.nightlight.enable = lib.mkDefault s.installed; services.hyprsunset = lib.mkIf (cfg.enable && !geo) { enable = true; settings.profile = [ # Daytime: identity = no colour change. { time = cfg.sunrise; identity = true; } # Night: shift to the warm temperature. { time = cfg.sunset; temperature = cfg.temperature; } ]; }; # 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.${lib.removeSuffix ".service" unit}.Service.ExecCondition = lib.mkIf cfg.enable "${nomarchy-nightlight}/bin/nomarchy-nightlight should-start"; home.packages = [ nomarchy-nightlight ]; }; }