feat(timezone): opt-in automatic, location-following timezone
nomarchy.system.autoTimezone (off by default): geoclue + 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); a System-menu entry toggles it. Because it's a SYSTEM service — not a user unit it can start/stop instantly like night-light — the toggle (nomarchy-autotimezone, run in a terminal) writes the flag --no-switch then drives `sudo nixos-rebuild` (bakes the service + the time.timeZone override) plus `home-manager switch` (the Waybar-refresh watcher); both sides read the one flag. - modules/nixos/timezone.nix: parses settings.autoTimezone from nomarchy.system.stateFile (like Plymouth), gates geoclue2 + automatic-timezoned, and mkForce-nulls time.timeZone — automatic- timezoned sets it null at normal priority, which collides with the installer's static value (a hard eval error), so force wins and reverts cleanly when disabled. Ships the nomarchy-autotimezone toggle. - modules/home/timezone.nix: a tiny user service watching timedate1's change signal that reloads Waybar (SIGUSR2) on a real zone change, so the clock follows live (Waybar captures the zone at construction). Gated on the same flag via nomarchy.settings.autoTimezone. - rofi menu: "Auto timezone (on/off)" in the System submenu. - option + template example + ROADMAP entry; theme.nix seeds the flag. Eval-verified on (geoclue+daemon on, tz forced null over a static installer value, watcher present) and off (static tz intact, nothing extra); home + system both build green. Needs an on-hardware check (geoclue detection + the SIGUSR2 clock refresh aren't CI-testable). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -492,6 +492,30 @@ how to override it. Items marked ✓ are shipped.
|
|||||||
sunset/sunrise (would mean wlsunset, which schedules by location). Pending an
|
sunset/sunrise (would mean wlsunset, which schedules by location). Pending an
|
||||||
on-machine check (first enable rebuilds + comes on; later toggles instant; off
|
on-machine check (first enable rebuilds + comes on; later toggles instant; off
|
||||||
persists across reboot via ExecCondition; stopping hyprsunset restores gamma).
|
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):**
|
- **Keyboard layouts (per-device + switching):**
|
||||||
- ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices`
|
- ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices`
|
||||||
(`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland
|
(`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
./yazi.nix # flagship TUI file manager, themed + plugins
|
./yazi.nix # flagship TUI file manager, themed + plugins
|
||||||
./osd.nix # swayosd volume/brightness OSD, themed
|
./osd.nix # swayosd volume/brightness OSD, themed
|
||||||
./nightlight.nix # scheduled blue-light filter (hyprsunset), opt-in
|
./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
|
./updates.nix # passive update-awareness indicator + notification, opt-in
|
||||||
./shell.nix # zsh + starship + bat/eza/zoxide, themed
|
./shell.nix # zsh + starship + bat/eza/zoxide, themed
|
||||||
./keys.nix # gpg-agent (fronts SSH) + pinentry-qt
|
./keys.nix # gpg-agent (fronts SSH) + pinentry-qt
|
||||||
|
|||||||
@@ -324,6 +324,14 @@ ${themeRows}
|
|||||||
&& exec nomarchy-nightlight toggle
|
&& exec nomarchy-nightlight toggle
|
||||||
notify-send "Night light" "Not available (nomarchy.nightlight off?)."; exit 0 ;;
|
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)
|
snapshot)
|
||||||
# btrfs-assistant 2.2 segfaults on 26.05 (libbtrfsutil ABI), so launch
|
# btrfs-assistant 2.2 segfaults on 26.05 (libbtrfsutil ABI), so launch
|
||||||
# the keyboard-driven snapper browser instead: a terminal running it
|
# the keyboard-driven snapper browser instead: a terminal running it
|
||||||
@@ -374,6 +382,10 @@ ${themeRows}
|
|||||||
then row "Night light (on)" weather-clear-night
|
then row "Night light (on)" weather-clear-night
|
||||||
else row "Night light (off)" weather-clear-night
|
else row "Night light (off)" weather-clear-night
|
||||||
fi
|
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
|
command -v nomarchy-snapshots >/dev/null 2>&1 && row "Snapshots" timeshift
|
||||||
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
|
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
|
||||||
row "Power profile" preferences-system-power
|
row "Power profile" preferences-system-power
|
||||||
@@ -388,6 +400,7 @@ ${themeRows}
|
|||||||
*Printers*) exec "$0" printers ;;
|
*Printers*) exec "$0" printers ;;
|
||||||
*"Do Not Disturb"*) exec "$0" dnd ;;
|
*"Do Not Disturb"*) exec "$0" dnd ;;
|
||||||
*"Night light"*) exec "$0" nightlight ;;
|
*"Night light"*) exec "$0" nightlight ;;
|
||||||
|
*"Auto timezone"*) exec "$0" autotimezone ;;
|
||||||
*Snapshots*) exec "$0" snapshot ;;
|
*Snapshots*) exec "$0" snapshot ;;
|
||||||
*"Power profile"*) exec "$0" power-profile ;;
|
*"Power profile"*) exec "$0" power-profile ;;
|
||||||
esac ;;
|
esac ;;
|
||||||
@@ -414,7 +427,7 @@ ${themeRows}
|
|||||||
esac ;;
|
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 ;;
|
exit 64 ;;
|
||||||
esac
|
esac
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ let
|
|||||||
settings = {
|
settings = {
|
||||||
nightlight = { installed = false; on = true; };
|
nightlight = { installed = false; on = true; };
|
||||||
keyboard.devices = { };
|
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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
44
modules/home/timezone.nix
Normal file
44
modules/home/timezone.nix
Normal file
@@ -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" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ let
|
|||||||
'';
|
'';
|
||||||
in
|
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 = {
|
config = {
|
||||||
# The safe half of distro branding: distroName flows into
|
# The safe half of distro branding: distroName flows into
|
||||||
|
|||||||
@@ -46,6 +46,16 @@
|
|||||||
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
|
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
|
||||||
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { 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 ''
|
snapper.enable = lib.mkEnableOption ''
|
||||||
hourly/daily BTRFS timeline snapshots of / via snapper, plus the
|
hourly/daily BTRFS timeline snapshots of / via snapper, plus the
|
||||||
`nixos-rebuild-snap` pre-rebuild-snapshot helper. No-op unless the
|
`nixos-rebuild-snap` pre-rebuild-snapshot helper. No-op unless the
|
||||||
|
|||||||
87
modules/nixos/timezone.nix
Normal file
87
modules/nixos/timezone.nix
Normal file
@@ -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;
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -31,6 +31,11 @@
|
|||||||
# thermal.enable = true; # thermald (Intel CPUs)
|
# 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)
|
# nomarchy.hardware = { # enablement above nixos-hardware (installer-detected)
|
||||||
# intel.enable = true; # GuC/HuC firmware; installer sets this on Intel
|
# intel.enable = true; # GuC/HuC firmware; installer sets this on Intel
|
||||||
# intel.computeRuntime = true; # OpenCL/oneVPL GPU compute (opt-in)
|
# intel.computeRuntime = true; # OpenCL/oneVPL GPU compute (opt-in)
|
||||||
|
|||||||
Reference in New Issue
Block a user