Home-Manager toggle between themes

I recently build a small home-manager thing that allows me to easily toggle themes:

You’ll notice a few things on invocation:

  • The most obvious one is probably Firefox doing a live reload of everything, even webpages if they are configured to use the system theme.

  • But also my terminal-emulator (kitty) doing the same and even the zsh-prompts (powerlevel10k) from past commands.

  • Waybar doesn’t live reload but my implementation just calls a home-manager activation script and thus the user systemd unit for waybar gets restarted and waybar parses it’s config again.

  • The icon theme changes from light to dark and back.

While doing so I encountered a few problems, qt applications don’t have a life reload (at least qdmr that’s the one I tested it with). FZF gets it theme through a env var I set in the zshrc and that doesn’t get live reloaded and I didn’t really find a fancy way of doing that and lastly chromium/electron is just deciding to be as crappy as usual and also won’t do a live reload which doesn’t really bother me too much since the only chromium application I use is element-desktop.

Now let’s go to the interesting part of how I implemented it:

Using @Misterio 's nix-colors I specified the dracula base16 theme as my default in home-manager as you would with any other option like:

  colorScheme = {
    slug = "dracula";
    colors = {
      base00 = "#282A36";
      base01 = "#3A3C4E";
      base02 = "#44475A";
      base03 = "#6272A4";
      base04 = "#62D6E8";
      base05 = "#F8F8F2";
      base06 = "#F1F2F8";
      base07 = "#80008E";
      base08 = "#FF5555";
      base09 = "#FFB86C";
      base0A = "#F1FA8C";
      base0B = "#50FA7B";
      base0C = "#8BE9FD";
      base0D = "#BD93F9";
      base0E = "#FF79C6";
      base0F = "#00F769";
    };
  };

and the used the values in other configs like the one for my notification daemon

{ config, ... }:

{
  services.mako = {
    enable = true;
    defaultTimeout = 10000;
    extraConfig = ''
    ignore-timeout=1
    anchor=bottom-right
    background-color=#${config.colorScheme.colors.base00}
    text-color=#${config.colorScheme.colors.base05}
    border-color=#${config.colorScheme.colors.base00}
    [urgency=low]
    border-color=#${config.colorScheme.colors.base00}
    [urgency=normal]
    border-color=#${config.colorScheme.colors.base0A}
    [urgency=high]
    border-color=#${config.colorScheme.colors.base08}
    '';
  };
}

To get the toggling capability I just defined something called a specialisation, you should note here that home-manager considers specialisation as experimental

  specialisation.light-theme.configuration = {
    # We have to force the values below to override the ones defined above
    colorScheme = lib.mkForce { 
      slug =  "catppuccinLatte";
      colors = {
        base00 = "#eff1f5";
        base01 = "#e6e9ef";
        base02 = "#ccd0da";
        base03 = "#bcc0cc";
        base04 = "#acb0be";
        base05 = "#4c4f69";
        base06 = "#dc8a78";
        base07 = "#7287fd";
        base08 = "#d20f39";
        base09 = "#fe640b";
        base0A = "#df8e1d";
        base0B = "#40a02b";
        base0C = "#179299";
        base0D = "#1e66f5";
        base0E = "#8839ef";
        base0F = "#dd7878";
      };
    };
    home.packages = with pkgs; [
      # note the hiPrio which makes this script more important then others and is usually used in nix to resolve name conflicts 
      (hiPrio (writeShellApplication {
      name = "toggle-theme";
      runtimeInputs = with pkgs; [ home-manager coreutils ripgrep ];
      # the interesting part about the script below is that we go back two generations
      # since every time we invoke a activation script home-manager creates a new generation 
      text =
        ''
          "$(home-manager generations | head -2 | tail -1 | rg -o '/[^ ]*')"/activate
        '';
      }))
    ];
  };

Other then that I also have a shorthand to activate the specialization:

  home.packages = with pkgs; [
    (writeShellApplication {
      name = "toggle-theme";
      runtimeInputs = with pkgs; [ home-manager coreutils ripgrep ];
      text =
        ''
          "$(home-manager generations | head -1 | rg -o '/[^ ]*')"/specialisation/light-theme/activate
        '';
    })
  ];

And in some places where color pattersn just don’t match or stuff like the gtk and icon theme I initially used if statements but then changed to “switch cases” after @ajs124 showed me them because they have a nicer syntax in this case:

  gtk = {
    enable = true;
    theme = { 
      dracula = {
        name = "Dracula";
        package = pkgs.dracula-theme;
      };
      catppuccinLatte = {
        name = "Catppuccin-Latte-Compact-Lavender-light";
        package = pkgs.catppuccin-gtk.override {
          accents = ["lavender"];
          size = "compact";
          variant = "latte";
        };
      };
    }.${config.colorScheme.slug};
    iconTheme = {
      package = pkgs.papirus-icon-theme;
      name = {
        dracula = "Papirus-Dark";
        catppuccinLatte = "Papirus-Light";
      }.${config.colorScheme.slug};
    };
  };

One last thing to note is that you have to set:

home-manager.useUserPackages = false;

in the NixOs module options otherwise the hiPrio thing above won’t work, thank you to @maralorn for helping me debug this, it was quite hard to find.

A disadvantage of this approach is that you basically double your home-manager evaluation time because everything gets evaluate twice (but the activation script finishes in less then a second)

And a advantage of this is that it does everything home-manager would usually do when doing a rebuild in terms of calling activation hooks and it feels quite native to integrate it in to a nix config.

@lassulus showed me his way of doing this which is:

Feel free to ask questions and give feedback, this is the first time where I write about something more implementation focused

12 Likes

Very nice! I’ve yet to properly integrate wpgtk into home-manager, it’s pretty similar but would do the activation “outside” of nix.

But nice that it also works with “native nix”!! Looks like it took quite some time to get there!

Ah good luck, I think I prefer the “native nix” way because it allows me to do more like for example reloading waybar.

Looks like it took quite some time to get there!

In total it was about ~6 hours or so

home-manager also a pywal module. Once enabled you can run wal --theme random_light or ``wal --theme random` to reload the supported programs (i3/sway/terminals etc)