Creating a Unix Theme Engine
In this article, I will walk through creating a unified UNIX theming engine that can take predefined themes and apply them to applications system-wide.
Other Theme Engines
I am not the first person to write some sort of automatic theme software. For example, /u/Dylan112 has created a very popular application called pywal It’s a beautiful piece of software that will create themes based on wallpapers and apply them to system applications. As much as I want to love the software I think it suffers from a lack of specificity.
Most famous color schemes (i.e. gruvbox, solarized, dracula) are carefully engineered
so that colors work together to create a cohesive yet functional look and feel.
Something like this is simply not possible with colors generated from a wallpaper.
For one, getting the right color palette from a wallpaper is nearly impossible.
But even more importantly, what do we do with these colors.
Maybe for one color scheme the color yellow is very subtle, and we want it to be
the border color for our active window. OK, I will tell my window manager to source
the color yellow from pywal
.
However, once I generate a new color scheme the color yellow might be very harsh.
Instead I want to use blue for my border color. Now I have to go and change my window
manager’s options…
You can see how this gets tedious very fast. Changing themes might require tweaking across dozens of applications and is totally unnecessary. To work around this problem I wrote a program called squash which allows users to define themes in bash scripts. These scripts can be sourced from any application and contain theme-specific information for how that application should appear.
In this article I want to explore writing this application and what I learned from it.
Creating a Theme File
Let’s start by identifying what we want our theme engine to do.
We want to define a set of colors and variables that can be used by different applications.
For no particular reason, I’m going to call this project squash
.
A regular squash
theme file will look something like this:
#!/usr/bin/env bash
# DEFINE COLORS
BG="#20292d"
FG="#c4c4b5"
BLK="#2c3539"
RED="#a66959"
GRN="#769070"
YLW="#ac8d6e"
BLU="#607a86"
MAG="#8a757e"
CYN="#60867f"
WHT="#c4c4b5"
BBLK="#343d41"
BRED="#a66959"
BGRN="#769070"
BYLW="#ac8d6e"
BBLU="#607a86"
BMAG="#8a757e"
BCYN="#60867f"
BWHT="#c4c4b5"
# ROFI OPTIONS
ACC="${YLW}"
# BAR OPTIONS
BAR_SCRIPT="tinybar"
# VIM COLORSCHEME
VIM_SCHEME='base16-default-dark'
# WALLPAPER OPTIONS
WALLPAPER_PATH="$HOME/Wallpapers/designr3.png"
WALLPAPER_STYLE="tile"
# 2BWM OPTIONS
TWOBWM_FOCUS="${CYN}"
TWOBWM_UNFOCUS="${BBLK}"
TWOBWM_FIXED="${RED}"
TWOBWM_UNKILL="${CYN}"
TWOBWM_FIXEDUNK="${MAG}"
TWOBWM_OUTR="${BG}"
TWOBWM_EMP="${BG}"
# BSPWM OPTIONS
BSPWM_NORMAL="${BBLK}"
BSPWM_FOCUSED="${CYN}"
BSPWM_URGENT="${RED}"
BSPWM_PRESEL="${GRN}"
OB_THEME="designr"
SQUASH_FONT="xft:Dina:pixelsize=12"
Wow, that seems like a lot of code!
Maybe, but its just enough for us to define our basic colors for something
like X11
and then define some application-specific colors.
Let’s dive into each part of this time file.
First, we notice that this is actually just a bash script #!/usr/bin/env bash
.
This is one of squash
’s defining features.
Instead of some werid format like json
or yaml
, this is just a simple bash
script that can be run by just about any shell program to get its variables.
This is great since shell scripts are the “glue” that hold UNIX programs together.
Anything from lemonbar
to bspwm
can read this format.
Next, we have our basic colors. Nothing too special here. Again this values can be used by any shell-scripting application.
Now we can start defining application-specific variables.
This is what makes squash
really special since we can really
engineer applications to look great with any colorscheme.
For example, if you are using the window manager bspwm
, you can source this
file and then define your active border color using BSPWM_FOCUSED
.
The actual value of BSPWM_FOCUSED
will vary depending on what theme is currently used.
But you only ever have to change themes, you never have to edit your bspwmrc
when you switch themes. That is awesome.
Making the Themes System Wide
We have defined our first theme, designr
, but now we need to get it out to applications.
I’m taking some inspiration from pywal
here, but we can create files of various formats
in $HOME/.cache/squash
so that applications have a constant location to source files from.
Not all applications are going to want shell scripts, so we should convert our squash
file into
as many formats as possible. For the applications I use most, I figured I needed to write a .css
file,
a file for .Xresources
, and a plain text file for other users to interact with.
The code for this isn’t very interesting, so I won’t post it here.
If you are interested in reading it, you can check out the write_XXXX
functions in squash
.
Applying the New Theme to Active Terminals
Probably 90% of my workflow is done within terminals. If I switch themes mid-session, the last thing I want to do is close and reopen all my terminals to get my new theme to take effect. Let’s write a pair of functions that will reload all active terminals with our new colors.
First, we can load our theme colors into an array so they are easier to iterate over:
COLOR_ARRAY=($BLK $RED $GRN $YLW $BLU $MAG $CYN $WHT \
$BBLK $BRED $BGRN $BYLW $BBLU $BMAG $BCYN $BWHT)
Now, let’s generate a set of escape sequences that something like urxvt
can use to update its internal state.
update_terminals() {
local sequence=""
for i in {0..15}; do
sequence+="\033]4;${i};${COLOR_ARRAY[${i}]}\007"
done
for i in 10 12 13; do
sequence+="\033]${i};${FG}\007"
done
for i in 11 14 708; do
sequence+="\033]${i};${BG}\007"
done
if [[ $SQUASH_FONT ]]; then
sequence+="\033]50;${SQUASH_FONT}\007"
sequence+="\033]711;${SQUASH_FONT}\007"
fi
reload_terminals "${sequence}"
}
All that’s left is to send this sequence to all active terminals.
reload_terminals() {
local seq="${1}"
for term in /dev/pts/[0-9]*; do
printf "%b" "${seq}" > "${term}"
done
}
Dynamically Reloading Neovim
For just about all software development I use neovim
.
Just like my terminals, I don’t want to close and reopen
neovim whenever I change themes. Sadly this is not possible with
vanilla neovim
. However, you can use neovim-remote
to simulate
a client-server model. This means you can send commands to neovim
without having that window or instance focused.
I’ll go ahead and assume we have neovim-remote
installed.
The first thing we need to do is make sure that anytime we open
neovim
we open it on a socket so we can talk to it remotely.
I added the following to my .zshrc
:
nvim() {
local fn="$(mktemp -u "/tmp/nvimsocket-XXXXXXX")"
NVIM_LISTEN_ADDRESS=$fn /usr/bin/nvim $@
}
This next part is VERY hacky.
I’m going to use sed
to change the value of my colorscheme
in my $MYVIMRC
. I can’t think of another way to make this
change safer sadly.
update_nvim() {
if [[ -z ${VIM_SCHEME} ]]; then
printf "Error: VIM_SCHEME not set: not changing vim colorscheme\n"
else
sed -i -e "s/colorscheme .*/colorscheme $VIM_SCHEME/g" $VIM_CONFIG
reload_nvim
fi
}
This changes the colorscheme
variable in neovim
to the value that we defined
in our squash
file. This is actually very convenient, however, since this means
that each squash
theme can use a different vim colorscheme. Cool!
All we have to do now is tell all instances of neovim
to source $MYVIMRC
.
reload_nvim() {
if [[ ! $(command -v nvr) ]]; then
printf "Error:: neovim-remote not found\n"
printf "Please install nvr to reload neovim\n"
else
inst=($(nvr --serverlist | grep nvim | sort | uniq))
for nvim_inst in "${inst[@]}"; do
nvr --servername "${nvim_inst}" --remote-send '<Esc>:so ${HOME}/.config/nvim/init.vim<CR>' &
done
fi
}
And we’re all done with neovim
. Upon switching themes we will change the active colorscheme
and reload all open instances.