While I'm aware that tinkering with the appearance of my editor, terminal or other apps won't make me any more productive, I like to waste spend time on it occasionally. Apart from my favorite font this mostly consists of tweaking the colorscheme. Finding nice colorschemes online is easy, the challenge is to find one that also supports all the applications you want to use. Non-matching colorschemes in different windows are, of course, a no-go.

Colorscheme demo

Matching colorschemes

Supposed you have managed to find a colorscheme you like that supports all applications you plan on using, you will still have to set them all up individually. Even worse, if you want to switch them up from to time you will have to modify all config files manually every time.

Base16

I'm not the fist to think there should be a better way to do this. Most prominently there is the base16 colorscheme framework. The idea is simple, define a configuration template for each application and let colorschemes be just a dictionary of 16 named colors that get put into the right places.

It is easy to contribute colorschemes or templates for new applications and every colorscheme will be compatible with all of them, since there is a defined spec.

I've been a base16 user for quite some time and even wrote a tool to manage all themes back when I was using arch. The need to write that already might tell you that there is still the problem of having to set up every application individually.

The switch to NixOS

The tool became useless the moment I switched to NixOS, since it is already the best tool for managing any kind of configuration on its own. I started to copy and paste base16 colors into my nix configs, but soon noticed another problem.

Semantic colors

Base16 themes consist of 16 colors named base00, base01 etc. The configuration templates then specify "color base05 should go here". Here is an example of such a theme, the bar for the i3 window manager:

bar {
    status_command i3status

    colors {
        background $base00
        separator  $base01
        statusline $base04

        # State             Border  BG      Text
        focused_workspace   $base05 $base0D $base00
        active_workspace    $base05 $base03 $base00
        inactive_workspace  $base03 $base01 $base05
        urgent_workspace    $base08 $base08 $base00
        binding_mode        $base00 $base0A $base00
    }
}

Every theme can define the colors to be whatever the author of that theme liked. This becomes a problem, when a color is meant to convey a certain meaning. Color base08 might be any beautiful color, but in the example above you would expect urgent_workspace to be something alerting, e.g. red. What I really wanted was a system of named colors, that then would be used across the system. Define what "red" is supposed to look once and have all things that should be red use that hexadecimal color.

Unix terminal emulators usually use 16 colors. To be more precise they use 8 colors in a normal and a bright variant. Base16 themes are obviously inspired by that scheme, and it seems to be a common denominator across many tools. Based on that, I decided on the following named colors, which should be enough to colorize all applications while keeping at least a bit of compatibility with existing themes:

  • Black
  • White
  • Yellow
  • Green
  • Cyan
  • Blue
  • Magenta
  • Red

In addition to that, every color will also have a "Bright" variant, e.g. "BrightRed". I also included bright variants for black and white to keep the names consistent, giving me a total of 4 shades of grey.

Global color definitions

While there are no real global variables in the nix language, you can define options that then can be referenced across the system configuration. For that I defined the following colorscheme.nix

{ lib, ... }:
with lib;
let
  colornames = [
    "Black" "BrightBlack" "White" "BrightWhite" "Yellow" "BrightYellow" "Green"
    "BrightGreen" "Cyan" "BrightCyan" "Blue" "BrightBlue" "Magenta"
    "BrightMagenta" "Red" "BrightRed"
  ];
in
{
  options.pinpox.colors = builtins.listToAttrs (map
    (c: {
      name = c;
      value = mkOption { type = types.str; };
    })
    colornames);
}

It defines a list of the color names and then maps over it to generate an option for each color. E.g. for "Blue" the option options.pinpox.colors.Blue will be created, which then can be set and accessed.

Current theme

For now, I have set the following colors, based on the catppuccin theme. A sample of the colors is in the header image of this post

  config.pinpox.colors = {
    Black = "24273a";
    BrightBlack = "5b6078";
    White = "cad3f5";
    BrightWhite = "747c9e";
    Red = "ed8796";
    BrightRed = "ff5370";
    Green = "a6da95";
    BrightGreen = "68f288";
    Yellow = "eed49f";
    BrightYellow = "fab387";
    Blue = "8aadf4";
    BrightBlue = "74c7ec";
    Magenta = "cba6f7";
    BrightMagenta = "f5bde6";
    Cyan = "8bd5ca";
    BrightCyan = "aee2da";
  };
}

Usage

With that out of the way, the colors can now be accessed across the system by importing the module. Here are a few examples.

Sway

The let ... in blog is only there to avoid littering the configuration with config.pinpox.colors every time a color is accessed and just replaces it with a local variable c. The colors then are just interpolated into the strings where you otherwise would have typed a hex value.

{
  colors =
    let
      c = config.pinpox.colors;
    in
    {

      focused = {
        background = "#${c.Blue}";
        border = "#${c.BrightBlue}";
        childBorder = "#${c.Blue}";
        indicator = "#${c.BrightBlue}";
        text = "#${c.Black}";
      };
    # ...
    };
}

Notification center

Some applications support defining variables in their configuration language. The notification center is styled with sass, allowing to just use the nix values to define sass variables. The rest of the configuration can then be placed in a dedicated file to avoid having to copy the full contents into your nix code

{
  xdg.configFile.swaync-style = {
    target = "swaync/style.css";
    
    text =
      let
        c = config.pinpox.colors;
      in
    ''
      @define-color Black #${c.Black};
      @define-color BrightBlack #${c.BrightBlack};
      @define-color White #${c.White};
      @define-color BrightWhite #${c.BrightWhite};
      @define-color Yellow #${c.Yellow};
      @define-color BrightYellow #${c.BrightYellow};
      @define-color Green #${c.Green};
      @define-color BrightGreen #${c.BrightGreen};
      @define-color Cyan #${c.Cyan};
      @define-color BrightCyan #${c.BrightCyan};
      @define-color Blue #${c.Blue};
      @define-color BrightBlue #${c.BrightBlue};
      @define-color Magenta #${c.Magenta};
      @define-color BrightMagenta #${c.BrightMagenta};
      @define-color Red #${c.Red};
      @define-color BrightRed #${c.BrightRed};

      ${fileContents ./style.https://github.com/catppuccin/catppuccincss}
    '';
}

Neovim

Vim and Neovim are a bit of a special case. I defined a template for a lua module that just contains the colors as variables which gets rendered to a file


local M =  {}

M.Black         = "#{{Black}}"
M.BrightBlack   = "#{{BrightBlack}}"

M.Red           = "#{{Red}}"
M.BrightRed     = "#{{BrightRed}}"
-- other colors ..

return M

From here it can be required in Neovim's lua configuration files.

v.g.colors_name = 'generated'
local nixcolors = require('nixcolors')

The last missing piece is colorbuddy, which allows defining colorschemes programmatically. I created a colorscheme template with it, you can find it here. It just maps color variables to highlighting groups and tree-sitter objects.

Results

I'll leave you with the obligatory neofetch screenshot, showing off matching zellij, neovim, sway, waybar themes and a wallpaper generated programmatically using the same colors.

Colorscheme demo