Migrate to hm managed theme switch

This commit is contained in:
Alexander
2026-07-03 17:31:31 +02:00
parent 60e23edf93
commit 27409eb5fa
8 changed files with 168 additions and 133 deletions
+61 -9
View File
@@ -1,4 +1,4 @@
{ config, lib, pkgs, ... }:
{ config, lib, pkgs, options, ... }:
with lib;
@@ -18,6 +18,35 @@ let
};
};
# Generate all theme-variant combinations
themeVariants = concatMapAttrs (name: theme: {
"${name}-dark" = { scheme = theme.dark; polarity = "dark"; };
"${name}-light" = { scheme = theme.light; polarity = "light"; };
}) cfg.themes;
# Base is gruvbox-dark, everything else is a specialisation
baseVariant = "gruvbox-dark";
specialisationVariants = filterAttrs (name: _: name != baseVariant) themeVariants;
availableVariants = concatStringsSep " " (attrNames themeVariants);
# Userspace theme switcher: activates a pre-built Home Manager
# specialisation generation. No sudo, no OS rebuild — just runs the
# specialisation's activation script in the base HM generation.
#
# The script logic lives in ./theme-switch.sh (plain shell, no Nix
# escaping); we only inject the two config-derived values it needs.
theme-switch = pkgs.writeShellScriptBin "theme-switch" ''
baseVariant="${baseVariant}"
availableVariants="${availableVariants}"
${builtins.readFile ./theme-switch.sh}
'';
# Stylix is only declared on hosts that import its Home Manager module
# (e.g. `fujin`). Servers (susano, izanagi, amaterasu, ...) don't, so
# `stylix` isn't declared there. We probe option *declarations* (not
# config values) to avoid an evaluation cycle.
hasStylix = options ? stylix;
in {
options.dov.dynamic-theme = {
enable = mkEnableOption "dynamic theme switching";
@@ -40,12 +69,35 @@ in {
};
};
config = mkIf cfg.enable {
# HM-level stylix config inherits from NixOS level
# Specialisations are handled by NixOS module
stylix = {
enable = true;
autoEnable = true;
};
};
config = mkIf cfg.enable (
{
home.packages = [ theme-switch ];
}
# Only reference the `stylix` option where it is actually declared.
# `mkIf false` is NOT enough: the module system still rejects a
# definition (even a disabled one) for an option that does not exist,
# which breaks every host that doesn't import stylix. `optionalAttrs`
# removes the key structurally, so non-stylix hosts evaluate cleanly.
// optionalAttrs hasStylix {
stylix = {
enable = true;
autoEnable = true;
# Base scheme/polarity; overridable per specialisation below.
base16Scheme = mkDefault themeVariants.${baseVariant}.scheme;
polarity = mkDefault themeVariants.${baseVariant}.polarity;
};
# Generate Home Manager specialisations for every non-base variant.
# Each is pre-built into the nix store during the single rebuild and
# activated userspace via `theme-switch` (no sudo, no OS rebuild).
specialisation = mapAttrs (name: variant: {
configuration = {
stylix = {
base16Scheme = mkForce variant.scheme;
polarity = mkForce variant.polarity;
};
};
}) specialisationVariants;
}
);
}
+77
View File
@@ -0,0 +1,77 @@
# shellcheck shell=bash
# theme-switch — activate a pre-built Home Manager specialisation.
#
# Expects two variables to be injected by the Nix wrapper:
# baseVariant the variant that is the generation itself (not a
# specialisation), activated via `$gen/activate`.
# availableVariants space-separated list of all variant names (help text).
#
# Standalone usage for testing:
# baseVariant=gruvbox-dark \
# availableVariants="gruvbox-dark gruvbox-light catppuccin-dark catppuccin-light" \
# bash theme-switch.sh <variant>
: "${baseVariant:?theme-switch: baseVariant not set}"
: "${availableVariants:?theme-switch: availableVariants not set}"
set -euo pipefail
variant="${1:-}"
if [ -z "$variant" ]; then
echo "Usage: theme-switch <variant>"
echo "Available: ${availableVariants}"
exit 1
fi
# Resolve the Home Manager *base* generation — the one built by NixOS that
# actually contains the `specialisation/` directory holding every variant's
# activation script.
#
# IMPORTANT: do NOT use `current-home` / the HM profile as the primary
# source. Activating a specialisation repoints those to the specialisation's
# *leaf* generation, which has no `specialisation/` of its own — so after
# switching once, every other variant (and the base) would become unfindable.
# The NixOS-managed `home-manager-«user».service` unit references the base
# generation and only changes on a rebuild.
user="${USER:-$(id -un)}"
gen=""
if unit="$(systemctl cat "home-manager-${user}.service" 2>/dev/null)"; then
gen="$(printf '%s\n' "$unit" | grep -oE '/nix/store/[0-9a-z]+-home-manager-generation' | head -n1)"
fi
# Fallbacks for standalone Home Manager, where the profile symlink / gcroot
# points at the base generation (not a specialisation leaf).
for candidate in \
"${HOME}/.local/state/home-manager/gcroots/current-home" \
"/nix/var/nix/profiles/per-user/${user}/home-manager" \
"${HOME}/.local/state/home-manager/home-manager-generation"
do
[ -n "$gen" ] && break
if [ -e "$candidate" ]; then
gen="$(readlink -e "$candidate")" || gen=""
fi
done
if [ -z "$gen" ]; then
echo "error: could not resolve Home Manager base generation" >&2
exit 1
fi
case "$variant" in
"${baseVariant}")
# Base variant is the generation itself, not a specialisation.
activate="$gen/activate"
;;
*)
activate="$gen/specialisation/$variant/activate"
;;
esac
if [ ! -x "$activate" ]; then
echo "error: variant '$variant' not found at $activate" >&2
echo "Available: ${availableVariants}"
exit 1
fi
echo "Switching theme to $variant..."
exec "$activate"