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.
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}}"
M. = "#{{BrightBlack}}"
M. = "#{{Red}}"
M. = "#{{BrightRed}}"
-- other colors ..
return M
From here it can be required in Neovim's lua configuration files.
v.. = 'generated'
local nixcolors = require
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.