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 :) ).
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.