diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 204158a..8563cea 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -492,6 +492,30 @@ how to override it. Items marked ✓ are shipped. 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). +- ✓ **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 + Waybar clock on its own. **Menu-driven, in-flake state** (the night-light / + keyboard philosophy): the flag is `settings.autoTimezone` in theme-state.json, + git-tracked; the System-menu entry (`nomarchy-menu autotimezone` → + `nomarchy-autotimezone`) writes it and rebuilds. **Not instant like + night-light** — it's a *system* service (not a user unit you can start/stop), + so the toggle drives a `sudo nixos-rebuild` (bakes the service + the + `time.timeZone` override) plus a `home-manager switch` (the Waybar-refresh + watcher), both off the one flag. **time.timeZone handling:** a runtime zone + needs `/etc/localtime` writable; automatic-timezoned sets `time.timeZone = null` + itself, but the installer's static value would collide (a hard eval error), so + the module forces it null (`mkForce`) over the installer's value when enabled, + and reverts to it when disabled. **Live clock refresh:** Waybar's clock module + captures the zone at construction, so `modules/home/timezone.nix` runs a tiny + user service that watches timedate1's change signal and reloads Waybar + (SIGUSR2) on a real zone change (also catches a manual `timedatectl + set-timezone`). Eval-verified both ways (on: geoclue+daemon on, tz forced null + over a static installer value, watcher present; off: static tz intact, no + service/watcher). **Pending an on-hardware check** — geoclue detection and the + SIGUSR2 clock refresh aren't testable in CI (if SIGUSR2 proves insufficient, + fall back to restarting waybar). Remaining (optional): a Waybar tooltip line + showing the detected zone. - **Keyboard layouts (per-device + switching):** - ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices` (`{ "" = { layout; variant; }; }`) generates Hyprland diff --git a/modules/home/default.nix b/modules/home/default.nix index 76a7009..ac2bd45 100644 --- a/modules/home/default.nix +++ b/modules/home/default.nix @@ -18,6 +18,7 @@ ./yazi.nix # flagship TUI file manager, themed + plugins ./osd.nix # swayosd volume/brightness OSD, themed ./nightlight.nix # scheduled blue-light filter (hyprsunset), opt-in + ./timezone.nix # keep the Waybar clock in step with auto-timezone changes ./updates.nix # passive update-awareness indicator + notification, opt-in ./shell.nix # zsh + starship + bat/eza/zoxide, themed ./keys.nix # gpg-agent (fronts SSH) + pinentry-qt diff --git a/modules/home/rofi.nix b/modules/home/rofi.nix index 48fcc49..523879b 100644 --- a/modules/home/rofi.nix +++ b/modules/home/rofi.nix @@ -324,6 +324,14 @@ ${themeRows} && exec nomarchy-nightlight toggle notify-send "Night light" "Not available (nomarchy.nightlight off?)."; exit 0 ;; + autotimezone) + # Toggle automatic timezone detection (geoclue + automatic-timezoned). + # A SYSTEM service, so flipping it writes the in-flake flag and runs a + # system rebuild (sudo) + a home switch — in a terminal, like Snapshots. + command -v nomarchy-autotimezone >/dev/null 2>&1 \ + || { notify-send "Auto timezone" "Unavailable on this machine."; exit 0; } + exec ${cfg.terminal} -e nomarchy-autotimezone toggle ;; + snapshot) # btrfs-assistant 2.2 segfaults on 26.05 (libbtrfsutil ABI), so launch # the keyboard-driven snapper browser instead: a terminal running it @@ -374,6 +382,10 @@ ${themeRows} then row "Night light (on)" weather-clear-night else row "Night light (off)" weather-clear-night fi + if [ "$(nomarchy-theme-sync get settings.autoTimezone 2>/dev/null)" = true ] + then row "Auto timezone (on)" preferences-system-time + else row "Auto timezone (off)" preferences-system-time + fi command -v nomarchy-snapshots >/dev/null 2>&1 && row "Snapshots" timeshift if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then row "Power profile" preferences-system-power @@ -388,6 +400,7 @@ ${themeRows} *Printers*) exec "$0" printers ;; *"Do Not Disturb"*) exec "$0" dnd ;; *"Night light"*) exec "$0" nightlight ;; + *"Auto timezone"*) exec "$0" autotimezone ;; *Snapshots*) exec "$0" snapshot ;; *"Power profile"*) exec "$0" power-profile ;; esac ;; @@ -414,7 +427,7 @@ ${themeRows} esac ;; *) - echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|printers|capture|keybinds|ask|dnd|nightlight|snapshot]" >&2 + echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|printers|capture|keybinds|ask|dnd|nightlight|autotimezone|snapshot]" >&2 exit 64 ;; esac ''; diff --git a/modules/home/theme.nix b/modules/home/theme.nix index 07c06e3..b9d9669 100644 --- a/modules/home/theme.nix +++ b/modules/home/theme.nix @@ -66,6 +66,10 @@ let settings = { nightlight = { installed = false; on = true; }; keyboard.devices = { }; + # Automatic timezone detection (nomarchy.system.autoTimezone): a system + # service, but the flag lives here so both sides read one source — the + # home side gates the Waybar-refresh watcher (timezone.nix) on it. + autoTimezone = false; }; }; diff --git a/modules/home/timezone.nix b/modules/home/timezone.nix new file mode 100644 index 0000000..d554c50 --- /dev/null +++ b/modules/home/timezone.nix @@ -0,0 +1,44 @@ +# Auto-timezone, home side: keep the Waybar clock in step with the system +# timezone. Waybar's clock module captures the zone once at construction, so a +# runtime timezone change (automatic-timezoned, nomarchy.system.autoTimezone) +# would NOT show until a relogin. A tiny watcher subscribes to timedate1's +# change signal and reloads Waybar (SIGUSR2 = the same reload theme-sync uses), +# so the clock follows your location live. Also catches a manual +# `timedatectl set-timezone`. +# +# Gated on the same in-flake flag the system side reads (settings.autoTimezone, +# exposed via nomarchy.settings) — so it only runs when the feature is on. The +# menu toggle (nomarchy-autotimezone) rebuilds both sides off that one flag. +{ config, lib, pkgs, ... }: + +let + cfg = config.nomarchy; + enabled = cfg.waybar.enable && (cfg.settings.autoTimezone or false); + + tzWatch = pkgs.writeShellScript "nomarchy-tz-watch" '' + last=$(${pkgs.systemd}/bin/timedatectl show -p Timezone --value 2>/dev/null || true) + ${pkgs.dbus}/bin/dbus-monitor --system \ + "type='signal',interface='org.freedesktop.DBus.Properties',path='/org/freedesktop/timedate1',member='PropertiesChanged'" \ + 2>/dev/null | + while read -r _; do + cur=$(${pkgs.systemd}/bin/timedatectl show -p Timezone --value 2>/dev/null || true) + [ "$cur" = "$last" ] && continue + last=$cur + ${pkgs.procps}/bin/pkill -SIGUSR2 -x waybar 2>/dev/null || true + done + ''; +in +{ + systemd.user.services.nomarchy-tz-watch = lib.mkIf enabled { + Unit = { + Description = "Reload Waybar on timezone change (auto-timezone)"; + PartOf = [ "graphical-session.target" ]; + After = [ "graphical-session.target" ]; + }; + Service = { + ExecStart = "${tzWatch}"; + Restart = "on-failure"; + }; + Install.WantedBy = [ "graphical-session.target" ]; + }; +} diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix index d88816f..347c65a 100644 --- a/modules/nixos/default.nix +++ b/modules/nixos/default.nix @@ -22,7 +22,7 @@ let ''; in { - imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ./services.nix ./hardware.nix ]; + imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ./services.nix ./hardware.nix ./timezone.nix ]; config = { # The safe half of distro branding: distroName flows into diff --git a/modules/nixos/options.nix b/modules/nixos/options.nix index 9815bbf..df99dc1 100644 --- a/modules/nixos/options.nix +++ b/modules/nixos/options.nix @@ -46,6 +46,16 @@ audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; }; bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = true; }; + autoTimezone.enable = lib.mkEnableOption '' + automatic timezone detection (geoclue + automatic-timezoned): the + system timezone — and so the Waybar clock — follows your location, so + travelling to another zone updates the time on its own. Off by default + (it's a location service and needs the network); toggle it from the + System menu, which lands the choice in the in-flake state file. Enabling + it unsets the static time.timeZone for you (a runtime timezone needs + /etc/localtime writable), so the menu toggle drives a system rebuild'' + // { default = false; }; + snapper.enable = lib.mkEnableOption '' hourly/daily BTRFS timeline snapshots of / via snapper, plus the `nixos-rebuild-snap` pre-rebuild-snapshot helper. No-op unless the diff --git a/modules/nixos/timezone.nix b/modules/nixos/timezone.nix new file mode 100644 index 0000000..5aa8d6c --- /dev/null +++ b/modules/nixos/timezone.nix @@ -0,0 +1,87 @@ +# Automatic timezone detection (opt-in) — the system timezone, and so the +# Waybar clock, follows your location, so travelling to another zone updates +# the time on its own. Geoclue feeds `automatic-timezoned`, which drives +# /etc/localtime at runtime. +# +# In-flake state, menu-driven (the keyboard/night-light philosophy): the on/off +# flag lives in the same theme-state.json under `settings.autoTimezone` +# (git-tracked, reproducible), written by the System-menu toggle +# (nomarchy-autotimezone). Because this is a SYSTEM service — not a user unit it +# can start/stop instantly like night-light — the toggle drives a system rebuild +# (plus a home switch for the Waybar-refresh watcher in timezone.nix home-side). +{ config, lib, pkgs, ... }: + +let + cfg = config.nomarchy.system; + + # Read the same state file the rest of the system side uses (Plymouth too), + # wired by lib.mkFlake. The flag defaults off when the file is absent/sparse. + state = + if cfg.stateFile != null + then builtins.fromJSON (builtins.readFile cfg.stateFile) + else { }; + stateEnabled = (state.settings or { }).autoTimezone or false; + + sync = lib.getExe pkgs.nomarchy-theme-sync; + + # Menu/CLI toggle. Runs as the normal user (it owns the flake checkout + + # writes the state); sudos only the system switch, like sys-update. Writes + # the in-flake flag, then rebuilds: the system rebuild bakes the service + + # the time.timeZone override, the home switch installs/removes the Waybar + # refresh watcher — both read the same flag we just wrote. + nomarchy-autotimezone = pkgs.writeShellScriptBin "nomarchy-autotimezone" '' + set -e + if [ "$(id -u)" -eq 0 ]; then + echo "nomarchy-autotimezone: run as your normal user (it sudos the rebuild itself)" >&2 + exit 1 + fi + flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}" + + cur=$(${sync} get settings.autoTimezone 2>/dev/null) || cur=false + case "''${1:-toggle}" in + on) new=true ;; + off) new=false ;; + toggle) case "$cur" in true|True) new=false ;; *) new=true ;; esac ;; + status) echo "$cur"; exit 0 ;; + *) echo "usage: nomarchy-autotimezone [toggle|on|off|status]" >&2; exit 64 ;; + esac + + ${sync} --quiet set settings.autoTimezone "$new" --no-switch + + if [ "$new" = true ]; then + notify-send "Auto timezone" "Enabling — rebuilding the system…" 2>/dev/null || true + else + notify-send "Auto timezone" "Disabling — rebuilding the system…" 2>/dev/null || true + fi + + sudo nixos-rebuild switch --flake "$flake#default" + home-manager switch --flake "$flake" + + if [ "$new" = true ]; then + notify-send "Auto timezone on" "The clock now follows your location." 2>/dev/null || true + else + notify-send "Auto timezone off" "Back to the fixed timezone in system.nix." 2>/dev/null || true + fi + ''; +in +{ + config = lib.mkMerge [ + { + # Shipped unconditionally so the menu can enable the feature even while + # it's off. Track the in-flake flag; mkDefault so a hand-set + # nomarchy.system.autoTimezone.enable in system.nix still wins. + environment.systemPackages = [ nomarchy-autotimezone ]; + nomarchy.system.autoTimezone.enable = lib.mkDefault stateEnabled; + } + + (lib.mkIf cfg.autoTimezone.enable { + services.geoclue2.enable = true; + services.automatic-timezoned.enable = true; + # A runtime timezone needs /etc/localtime writable. automatic-timezoned + # sets time.timeZone = null itself, but the installer writes a static + # value at normal priority, which would collide (a hard eval error) — + # mkForce null overrides both and resolves cleanly. + time.timeZone = lib.mkForce null; + }) + ]; +} diff --git a/templates/downstream/system.nix b/templates/downstream/system.nix index 98d206c..6ce4b4a 100644 --- a/templates/downstream/system.nix +++ b/templates/downstream/system.nix @@ -31,6 +31,11 @@ # thermal.enable = true; # thermald (Intel CPUs) # }; # + # nomarchy.system.autoTimezone.enable = true; # clock follows your location + # # (geoclue); travelling updates the + # # time on its own. Unsets time.timeZone + # # above. Easier toggled from System menu. + # # nomarchy.hardware = { # enablement above nixos-hardware (installer-detected) # intel.enable = true; # GuC/HuC firmware; installer sets this on Intel # intel.computeRuntime = true; # OpenCL/oneVPL GPU compute (opt-in)