218 lines
8.3 KiB
Nix
218 lines
8.3 KiB
Nix
{ config, lib, pkgs, options, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.dov.dynamic-theme;
|
|
schemes = "${pkgs.base16-schemes}/share/themes";
|
|
|
|
# Single source of truth for builtin themes
|
|
builtinThemes = {
|
|
gruvbox = {
|
|
dark = "${schemes}/gruvbox-dark-hard.yaml";
|
|
light = "${schemes}/gruvbox-light-hard.yaml";
|
|
};
|
|
catppuccin = {
|
|
dark = "${schemes}/catppuccin-mocha.yaml";
|
|
light = "${schemes}/catppuccin-latte.yaml";
|
|
};
|
|
};
|
|
|
|
# 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);
|
|
|
|
# `variant:schemeFile` pairs so the wallpaper picker can read each variant's
|
|
# base16 palette at runtime.
|
|
variantSchemes = concatStringsSep " "
|
|
(mapAttrsToList (variant: v: "${variant}:${v.scheme}") themeVariants);
|
|
|
|
# variant:doomtheme pairs consumed by the theme-switch script's
|
|
# best-effort Doom Emacs hook (see ./theme-switch.sh).
|
|
doomThemeMap = concatStringsSep " "
|
|
(mapAttrsToList (variant: theme: "${variant}:${theme}") cfg.doomThemes);
|
|
|
|
# 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;
|
|
|
|
# --- Wallpaper handling (runtime-cloned repo + palette matching) ----------
|
|
wallpapersEnabled = cfg.wallpaperRepo != null;
|
|
wallpapersDir = "${config.home.homeDirectory}/Wallpapers";
|
|
# hyprpaper's config points here; set-wallpaper rebinds this symlink, so we
|
|
# never have to write hyprpaper config ourselves.
|
|
wallpaperSymlink = "${config.xdg.stateHome}/theme/wallpaper-current";
|
|
|
|
wallpaper-pick = pkgs.writeShellScriptBin "wallpaper-pick" ''
|
|
SCRIPT_TAG="wallpaper-pick"
|
|
${builtins.readFile ./log.sh}
|
|
wallpapersDir="${wallpapersDir}"
|
|
wallpaperRepo="${cfg.wallpaperRepo or ""}"
|
|
variantSchemes="${variantSchemes}"
|
|
MAGICK="${getExe pkgs.imagemagick}"
|
|
GIT="${getExe pkgs.git}"
|
|
${builtins.readFile ./wallpaper-pick.sh}
|
|
'';
|
|
|
|
wallpaper-set = pkgs.writeShellScriptBin "wallpaper-set" ''
|
|
SCRIPT_TAG="wallpaper-set"
|
|
${builtins.readFile ./log.sh}
|
|
wallpaperSymlink="${wallpaperSymlink}"
|
|
${builtins.readFile ./set-wallpaper.sh}
|
|
'';
|
|
|
|
# Applied once at login (after hyprpaper) to seed a base-variant wallpaper.
|
|
wallpaper-init = pkgs.writeShellScript "wallpaper-init" ''
|
|
wp="$(${getExe wallpaper-pick} ${baseVariant} 2>/dev/null)" || exit 0
|
|
[ -n "$wp" ] && ${getExe wallpaper-set} "$wp" || true
|
|
'';
|
|
|
|
# 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 config-derived values it needs.
|
|
theme-switch = pkgs.writeShellScriptBin "theme-switch" ''
|
|
SCRIPT_TAG="theme-switch"
|
|
${builtins.readFile ./log.sh}
|
|
baseVariant="${baseVariant}"
|
|
availableVariants="${availableVariants}"
|
|
doomThemes="${doomThemeMap}"
|
|
wallpapersEnabled="${if wallpapersEnabled then "1" else "0"}"
|
|
wallpaperPickBin="${if wallpapersEnabled then getExe wallpaper-pick else ""}"
|
|
wallpaperSetBin="${if wallpapersEnabled then getExe wallpaper-set else ""}"
|
|
${builtins.readFile ./theme-switch.sh}
|
|
'';
|
|
in {
|
|
options.dov.dynamic-theme = {
|
|
enable = mkEnableOption "dynamic theme switching";
|
|
|
|
themes = mkOption {
|
|
type = types.attrsOf (types.submodule {
|
|
options = {
|
|
dark = mkOption {
|
|
type = types.path;
|
|
description = "Path to dark variant base16 scheme";
|
|
};
|
|
light = mkOption {
|
|
type = types.path;
|
|
description = "Path to light variant base16 scheme";
|
|
};
|
|
};
|
|
});
|
|
default = builtinThemes;
|
|
description = "Available themes with dark and light variants";
|
|
};
|
|
|
|
doomThemes = mkOption {
|
|
type = types.attrsOf types.str;
|
|
default = {
|
|
gruvbox-dark = "doom-gruvbox";
|
|
gruvbox-light = "doom-one-light";
|
|
catppuccin-dark = "doom-one";
|
|
catppuccin-light = "doom-one-light";
|
|
};
|
|
description = ''
|
|
Mapping from theme variant name (e.g. "gruvbox-dark") to the Doom
|
|
Emacs theme symbol (e.g. "doom-gruvbox") that theme-switch will
|
|
live-load via emacsclient when switching to that variant. Variants
|
|
absent from this map are left unchanged in Emacs.
|
|
|
|
Only themes actually installed in Doom will load; the defaults use
|
|
themes bundled with doom-themes (so they work out of the box, though
|
|
catppuccin variants fall back to generic dark/light). For accurate
|
|
catppuccin colours, install the `catppuccin-theme` Emacs package and
|
|
map the variants to `catppuccin-mocha` / `catppuccin-latte`.
|
|
'';
|
|
};
|
|
|
|
wallpaperRepo = mkOption {
|
|
type = types.nullOr types.str;
|
|
default = null;
|
|
example = "https://example.org/me/wallpapers.git";
|
|
description = ''
|
|
Git URL of a wallpaper repository. When set, theme-switch also picks
|
|
a random wallpaper whose palette matches the current variant and
|
|
applies it via hyprpaper. The repo is cloned to
|
|
`~/Wallpapers` on first use (a one-time, runtime cost — it is NOT
|
|
fetched at build time, so `nix build` stays fast).
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable (mkMerge [
|
|
{
|
|
home.packages = [ theme-switch ]
|
|
++ optionals wallpapersEnabled [ wallpaper-pick wallpaper-set ];
|
|
}
|
|
|
|
# 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`
|
|
# (keyed on `options ?`, i.e. *declarations* — never a config value) is
|
|
# the recursion-free way to gate structure on stylix presence.
|
|
(optionalAttrs hasStylix {
|
|
stylix = {
|
|
enable = true;
|
|
autoEnable = true;
|
|
# Base scheme/polarity; overridable per specialisation below.
|
|
base16Scheme = mkDefault themeVariants.${baseVariant}.scheme;
|
|
polarity = mkDefault themeVariants.${baseVariant}.polarity;
|
|
# When we manage hyprpaper's wallpaper ourselves, disable stylix's
|
|
# hyprpaper target so the two don't fight over the wallpaper path.
|
|
targets.hyprpaper.enable = mkForce (!wallpapersEnabled);
|
|
};
|
|
|
|
# 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;
|
|
})
|
|
|
|
# Wallpaper handling. These options always exist structurally (HM ships
|
|
# services.hyprpaper and a freeform systemd.user.services), so we gate
|
|
# the *values* with mkIf/optionals rather than shaping structure — that
|
|
# avoids the config-depends-on-config recursion that optionalAttrs on a
|
|
# config value would cause.
|
|
{
|
|
services.hyprpaper.enable = wallpapersEnabled;
|
|
services.hyprpaper.settings = mkIf wallpapersEnabled {
|
|
splash = false;
|
|
wallpaper = [{ monitor = ""; path = wallpaperSymlink; }];
|
|
};
|
|
|
|
systemd.user.services.wallpaper-init = mkIf wallpapersEnabled {
|
|
Unit = {
|
|
Description = "Pick and apply a wallpaper matching the base theme";
|
|
After = [ "hyprpaper.service" ];
|
|
PartOf = [ "graphical-session.target" ];
|
|
};
|
|
Service = {
|
|
Type = "oneshot";
|
|
ExecStart = "${wallpaper-init}";
|
|
};
|
|
Install.WantedBy = [ "graphical-session.target" ];
|
|
};
|
|
}
|
|
]);
|
|
}
|