Felix Kling

Automatically switch color schemes in i3, kitty, neovim and GTK applications

I’ve been using a light theme (Solarized) for the last couple of years and I think it works great during the day. But I found it to be too intense for the evening or night.

I made an attempt to implement a “dark mode toggle” a while ago, but because I couldn’t automate the switch for every application, I didn’t really use it. I recently switched from from urxvt to kitty as my terminal emulator and from vim to neovim and wanted to give it another try. This post describes the result of this effort (spoiler: everything is automated now :) ).

Desktop with Solarized Light Desktop with Solarized Dark
Desktop with Solarized Light and Dark

Final Script #

This is the script I’m currently using and will explain in the following sections:

#!/bin/sh

# Determine mode
if [ "$(readlink ~/.Xresources.colors)" = ".Xresources.solarized.light" ]; then
  next_background=dark
else
  next_background=light
fi

# A specific mode (light/dark) can be forced from the command line
if [ -n "$1" ] && [ "$1" != "$next_background" ]; then
  # This is not that intuitive but if the requested mode is different from the
  # next mode then the _current_ mode is the same as the requested one and there
  # is nothing to do
  exit 0
fi

# Update color files
# i3 and other applications that use X resources. The symlink is used to load
# the correct color scheme on startup (.Xresources includes .Xresources.colors
# via #include .Xresources.colors)
ln -sf ".Xresources.solarized.$background" ~/.Xresources.colors
# Overwrite color configuration
xrdb -merge ~/.Xresources.colors
# Vim
echo "set background=$background" > ~/.vimrc.color
# Rofi
echo "solarized-$background" > ~/.config/rofi/theme
# For triggering dark themes in GTK apps. Requires `gnome-themes-extra` to be
# installed (for adwaita-dark). This is primarily for Firefox
if [ $background = dark ]; then
  sed -i s/Adwaita/Adwaita-dark/ ~/.xsettingsd
else
  sed -i s/Adwaita-dark/Adwaita/ ~/.xsettingsd
fi


# Reload
# Update WM background
xsetroot -solid "$(xrdb -query | grep 'background' | head -n1 | cut -f 2)"
# Update Gtk apps
killall -HUP xsettingsd
# Update i3
i3-msg reload
# Update terminal emulator
kitty +kitten themes --reload-in=all "Solarized $(echo $background | sed 's/./\U&/')"
# Vim watches ~/.vimrc.color and reloads itself

The script shows that there are two parts to the problem: Updating color configuration files and signaling applications to reload those files. For better or worse, the exact steps necessary differ from application to application.

To toggle the color scheme I also need to know which scheme is currently used. There are multiple ways to do this (e.g. inspecting one of the color configuration files). I decided to check which file .Xresources.color links to.

Kitty’s built-in solution (easy) #

Since v0.23 (2021-08-16) kitty has built-in support for themes and most importantly: It is able to update the color scheme in all running instances. There are no color files to update, kitty does that automatically for you. All you have to do is run the following command:

kitty +kitten themes --reload-in=all <color scheme>

(NOTE: You can create your own color schemes or overwrite existing ones by storing them in ~/.config/kitty/themes/).

rofi (also easy) #

Because rofi is run on demand, we don’t have to signal the application. We only have to tell rofi which color scheme to use, which is done via the --theme argument.
I have two configuration files for the different color schemes in ~/.config/rofi/. The name of the file is stored in ~/.config/rofi/theme, which is updated by the toggle script:

echo "solarized-$background" > ~/.config/rofi/theme

Rofi is started via an i3 keybinding:

bindsym Mod1+space exec rofi -theme $(cat ~/.config/rofi/theme) ...

i3 (and other apps that use Xresources) #

I’m trying to reduce complexity as much as possible, which in this case means to reduce the number of places where color information is stored. Because I used to use urxvt, using X resources was the way to go.

As you can see in the script, I have two color configuration files (.Xresources.solarized.*), which contain settings in the form of

*background:            ...
*background-highlight:  ...

The * at the beginning allows the setting to be loaded/queried with any prefix (usually applications use the their name as prefix).

The color information can be updated directly with xrdb -merge .Xresources.solarized...., but in order to load the correct color scheme on startup, .Xresources.colors links to one of them and .Xresources includes .Xresources.colors via

#include '.Xresources.colors'

For #include to work you need to have a pre-processor installed, such as cpp.

Unlike urxvt, i3 does not use X resources by default but provides a way to query it in the configuration file:

set_from_resource $background i3.background
set_from_resource $background-h i3.background-highlight
set_from_resource $black i3.color0
...
client.focused          $background-h $background-h $foreground $red $cyan
...

After updating the symlink and updating the X resources database with xrdb, i3 can be signaled to reload its configuration file with i3-msg:

i3-msg reload

GTK applications (Firefox, …) #

This took me the longest to get to work. GTK can be configured to use a dark theme or tell applications to perfer a dark theme:

# ~/.config/gtk-3.0/settings.ini
[Settings]
gtk-application-prefer-dark-theme=true

but changing these settings doesn’t cause applications to update at runtime.

The solution to that problem is to use Xsettingsd for configuration instead. It’s a substitute for the Gnome settings daemon. When applications get their settings from the daemon they can be signaled to update those settings. However, not all settings can be set this way. E.g. I couldn’t get the aforementioned gtk-application-prefer-dark-theme setting to work. What did work however was switching between a dark and light theme via

# ~/.xsettingsd

Net/ThemeName "Adwaita"
# Net/ThemeName "Adwaita-dark"

But for this to work, the dark theme also needs to be installed, which on Arch Linux is part of the gnome-themes-extra package (I didn’t have this package and didn’t understand why it didn’t work).

The following settings did not work (note that I don’t know the difference between the Net and the Gtk prefix so I tried all of them):

# ~/.xsettingsd

Net/ApplicationPreferDarkTheme 1
Gtk/ApplicationPreferDarkTheme 1
# Appending the theme variant doesn't work via xsettingsd but it worked in
# settings.ini, even without the dark theme installed
Net/ThemeName "Adwaita:dark"
Gtk/ThemeName "Adwaita:dark"

To update .xsettingsd I’m using simple sed commands:

if [ $background = dark ]; then
  sed -i s/Adwaita/Adwaita-dark/ ~/.xsettingsd
else
  sed -i s/Adwaita-dark/Adwaita/ ~/.xsettingsd
fi

The xsettings daemon is informed about changes via the HUP signal:

killall -HUP xsettingsd

This should update running GTK applications, but I have only tested it with Firefox and gnucash.

Neovim #

The color scheme I’m using (vim-solarized8) determines which theme (light/dark) to use based on the value of the background settings.

To make things easier to update, I’m setting the this option in a separate file:

echo "set background=$background" > ~/.vimrc.color

This file is loaded in my vim setup. And ideally vim would reload it every time this file changes.
Luckily neovim provides an API to do just that, including an example (:help watch-file).

I created a Lua module which sets up the file watcher (following the example) to source .vimrc.color whenever it changes:

local colorFile = vim.fn.expand('~/.vimrc.color')
local function reload() 
	vim.cmd("source ".. colorFile)
end

local w = vim.loop.new_fs_event()
local on_change
local function watch_file(fname)
	w:start(fname, {}, vim.schedule_wrap(on_change))
end
on_change = function()
	reload()
	-- Debounce: stop/start.
	w:stop()
	watch_file(colorFile)
end

-- reload vim config when background changes
watch_file(colorFile)
reload()

vim.opt.termguicolors = true
vim.cmd("colorscheme solarized8")

Wrapping up #

I associated the script with a keyboard shortcut in i3:

bindsym Mod1+F3 exec ~/bin/togglecolors.sh

and I setup systemd timers to automatically switch between light and dark mode at the beginning and the end of the day. The service unit is straightforward it just runs the script with the provided parameter (making use of service instances):

# ~/.config/systemd/user/[email protected]
[Unit]
Description=Update colorschme

[Service]
ExecStart=%h/bin/togglecolors.sh %I

with the timer units being:

# ~/.config/systemd/user/lightmode.timer
[Unit]
Description=Switch to light mode

[Timer]
OnCalendar=Mon..Sun 8:00
Persistent=true
Unit=[email protected]

[Install]
WantedBy=timers.target
# ~/.config/systemd/user/darkmode.timer
[Unit]
Description=Switch to dark mode

[Timer]
OnCalendar=Mon..Sun 20:00
Persistent=true
Unit=[email protected]

[Install]
WantedBy=timers.target

An interesting improvement to this would be to make the timers relative to the local sunrise and sunset times.