# 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 ` 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; 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"; }; }; 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; }; 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 ''; }; }