Files
Nomarchy/modules/home/theme.nix
Bernardo Magri f3325385c1 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>
2026-06-23 10:34:45 +01:00

120 lines
4.9 KiB
Nix

# Nomarchy theming engine.
#
# nomarchy.stateFile (theme-state.json, inside the consuming flake) is
# ingested at evaluation time — pure, because flake files are store
# paths — and exposed to every other module as `config.nomarchy.theme`.
#
# Theme changes are fully Home Manager managed: `nomarchy-theme-sync
# apply <theme>` writes the JSON and runs `home-manager switch`, baking
# everything (Hyprland, Waybar, Ghostty, btop, Stylix) into one
# read-only generation. No runtime patching, no partial states; theme
# history is generation history.
#
# The one runtime exception is the wallpaper (swww is imperative by
# nature): applied at session start, after every switch (hook below),
# and cycled instantly with `nomarchy-theme-sync bg next`.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy;
themeState = builtins.fromJSON (builtins.readFile cfg.stateFile);
# Defaults guarantee evaluation succeeds on a sparse or older state
# file (e.g. one written before a schema field was added). The shipped
# Tokyo Night preset provides the color/ansi fallbacks; the path is
# relative to this module, so it resolves inside Nomarchy's own flake
# source even when consumed downstream.
preset = builtins.fromJSON (builtins.readFile ../../themes/tokyo-night.json);
defaults = preset // {
fonts = {
mono = "JetBrainsMono Nerd Font";
ui = "Inter";
size = 11;
};
ui = {
gapsIn = 5;
gapsOut = 12;
borderSize = 2;
rounding = 10;
iconSize = 36; # rofi launcher + menu icon size (px) for the generated theme
activeOpacity = 1.0;
inactiveOpacity = 0.95;
terminalOpacity = 0.96;
blur = true;
shadow = true;
};
# Icon theme name; "" means "pick by mode" (resolved below). A preset
# or the state file can name any theme in the shipped icon package.
icons = "";
# Window border colors. Values are palette keys (resolved against
# `colors` below) or literal #rrggbb. Solid, not a gradient — every
# legacy identity theme used a solid border (accent for most, the text
# tone for kanagawa/summer-*). Each preset declares its own so a theme
# switch always replaces it (deep_merge would otherwise leave it stuck).
border = { active = "accent"; inactive = "overlay"; };
# Non-appearance feature settings the menu/watchers write into this same
# in-flake state. nomarchy.nightlight: `installed` (sticky — gates the unit,
# so the first enable rebuilds) and `on` (instant runtime on/off).
# settings.keyboard.devices: per-device layouts the new-keyboard watcher
# remembers (device-name -> XKB layout), git-tracked instead of
# ~/.local/state; they graduate into nomarchy.keyboard.devices on the next
# rebuild. Defaulted so a sparse/older state file still evaluates;
# nomarchy.settings exposes them.
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;
};
};
parsed = lib.recursiveUpdate defaults themeState;
# A border value is a palette key (look it up in colors) unless it's
# already a literal hex; unknown keys fall through to the raw string.
resolveColor = v: if lib.hasPrefix "#" v then v else parsed.colors.${v} or v;
border = {
active = resolveColor parsed.border.active;
inactive = resolveColor parsed.border.inactive;
};
# Resolve the icon theme once and expose it on nomarchy.theme so both
# the GTK side (stylix.nix) and rofi (rofi.nix) agree. The shipped
# package (papirus-icon-theme) carries the Dark/Light pair.
iconTheme =
if parsed.icons != "" then parsed.icons
else if parsed.mode == "light" then "Papirus-Light"
else "Papirus-Dark";
in
{
config = {
nomarchy.theme = parsed // { inherit iconTheme border; };
# Feature toggles the menu writes (settings.nightlight.enable, …), exposed
# alongside the appearance state. Feature modules mkDefault-read from here
# so a menu toggle lands in the flake instead of in ~/.local/state.
nomarchy.settings = parsed.settings;
nomarchy.lib = {
# "#7aa2f7" -> "rgb(7aa2f7)" (Hyprland color syntax)
rgb = c: "rgb(${lib.removePrefix "#" c})";
# "#16161e" -> "rgba(16161eaa)" (Hyprland color + hex alpha)
rgba = c: alpha: "rgba(${lib.removePrefix "#" c}${alpha})";
};
home.packages = [ cfg.package ];
# After a switch the new theme's wallpaper should show without
# logging out. Best-effort: outside a graphical session swww isn't
# running and this is a no-op.
home.activation.nomarchyWallpaper = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
run ${lib.getExe cfg.package} --quiet wallpaper || true
'';
};
}