Home-manager specializations for light and dark themes

Has anyone here successfully used home-manager specializations (rather than NixOS specialisations) to switch between themes or light/dark modes during runtime with an activation script?

I found this post and looked through rycee’s config to replicate it for myself.

However, I use home-manager as a module in my NixOS flake rather than standalone, which might be conflicting somehow with specializations.

For example, I have options like this:

  options = {
    theme = {
      colors = lib.mkOption {
        type = lib.types.attrs;
        description = "Base16 color scheme.";
        default = (import ../modules/colorscheme/gruvbox).dark;
      };
      dark = lib.mkOption {
        type = lib.types.bool;
        description = "Enable dark mode.";
        default = true;
      };
    };
  };

And config like this:

  config = {
    home-manager.users.${config.user}.specialization.light.configuration = {
      theme = {
        colors = (import ../modules/colorscheme/gruvbox).light;
        dark = false;
      };
    };
  };

But I get errors when rebuilding:

error: The option `home-manager.users.noah.specialization.light.configuration.theme' does not exist.

you misspelled specialisation, everything else looks right, though.

It was actually renamed in 2023 in Home-Manager from specialization.

I haven’t tried it in a few years, though, so it might be worth revisiting to see if it works now. :slight_smile:

oh! that’s absolutely my bad. i use them and they seem to work pretty well, but some fiddling is required for a command to switch between specialisations.

How do you switch between home-manager specializations?

It is a bit fiddly, as you have to figure out the location of the activation script yourself, but it is described in the HM manual:

https://nix-community.github.io/home-manager/options.xhtml#opt-specialisation

1 Like

It is a bit fiddly indeed. The problem is that home-manager specialisations don’t account for such use case. They expect you to “rebuild and switch” into your specialisations, but don’t provide activation links to them on every home-manager rebuild, so that you could instantly toggle another specialisation that would include all your recent changes built when in different specialisations as well. That means, that it requires to intervene in between home-manager building and activation and set up activation links there. Neither home-manager or nh support this natively yet.

I have home-manager in standalone installation mode though. For home-manager as nixos module, I didn’t test this since I was using only nixos specialisations when running “home-manager as module” setup before. There, home-manager configuration can be included in nixos modules and there is therefore not much need to use home-manager specialisations at all.

My base config includes all theming for the dark mode. Then I created a dummy specialisation for it. I am using nh and as instructed on nh git page, the marker file here in ~/.local/share/home-manager/specialisation is for nh to handle specialisations better, which is also leveraged in the rebuild script below:

dark

specialisation.dark.configuration = {
  xdg.dataFile."home-manager/specialisation".text = "dark"; 
};

Then I created the light mode specialisation, which overrides settings related to theming inherited from the base config to light values instead:

light

specialisation.light.configuration = {
  xdg.dataFile."home-manager/specialisation".text = "light"; 
  stylix.base16Scheme = lib.mkForce light-pallete; 
  stylix.polarity = lib.mkForce "light";
  gtk = {
    gtk3.extraConfig.gtk-application-prefer-dark-theme = lib.mkForce false;
    gtk4.extraConfig.gtk-application-prefer-dark-theme = lib.mkForce false; 
  };
  dconf.settings = lib.mkForce { "org/gnome/desktop/interface" = { color-scheme = "prefer-light"; }; }; 
  ...
  ...
  ...
};

I replaced nh home switch command with the following script. That, is I always use this script to rebuild my home-manager config. It symlinks fresh activation links to dark and light specialisations at $HOME on every rebuild independently from whether I’m building from within the base, light or dark specialisation, so that they are always available to switch into them.

rebuild script

#!/usr/bin/env bash
set -euo pipefail

spec_marker="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager/specialisation"
explicit="${3:-}"   # optional: mmm nix hm dark
result="$(mktemp -u /tmp/hm-result-XXXXXX)"

# always builds config.home.activationPackage (the base) — never activates
nh home build --out-link "$result" || { rm -f "$result"; exit 1; }

# Resolve to the real store path so the persistent symlink survives
# deletion of the temp out-link below.
real_result="$(readlink -f "$result")"

if [[ -e "$real_result/specialisation" ]]; then
  [[ -L "$HOME/.specialisation" ]] && unlink "$HOME/.specialisation"
  ln -s "$real_result/specialisation" "$HOME/.specialisation"
fi

# pick target: explicit arg > current marker > base
spec="${explicit:-$(cat "$spec_marker" 2>/dev/null || true)}"

if [[ -n "$spec" ]]; then
  if [[ -e "$real_result/specialisation/$spec" ]]; then
    "$real_result/specialisation/$spec/activate"
    echo "$spec" > "$HOME/.cache/theme-marker" # update marker file for toggle script as well
  else
    echo "error: '$spec' is not a valid specialisation" >&2
    rm -f "$result"
    exit 1
  fi
else
  "$real_result/activate"
fi

rm -f "$result"
notify-send 'Home rebuild finished'

Then I toggle between specialisations like this. Marker file for nh cannot be used here, as it is being written by home-manager itself and only exists within the specialisations, but not inside the base. Therefore, from home-manager independent new marker file needed to be created here for toggling:

toggle script

!/usr/bin/env bash
set -euo pipefail

MARKER="$HOME/.cache/theme-marker"
current=$(cat "$MARKER" 2>/dev/null)

if [ "$current" == "light" ]; then
  target="dark"
  brightnessctl set 10%
  echo "Dark Mode"
  notify-send "Dark Mode"
else
  target="light"
  brightnessctl set 75%
  echo "Light Mode"
  notify-send "Light Mode"
fi

# activate specialialisation and change marker file
"$HOME/.specialisation/$target/activate" && echo "$target" > "$MARKER"